Pure Blade + Alpine.js + DaisyUI components for Laravel ERP/CRM interfaces.
No Livewire. No magic. No hidden server roundtrips.
composer require edumicro/daisybladeDaisyBlade is the evolution of edumicro/daisylw4, rebuilt without Livewire. The stack is intentionally boring:
- Blade renders structure
- Alpine.js manages local UI state
- Axios handles explicit server calls
- DaisyUI 5 provides the design system
Every server interaction is a plain Laravel controller returning JSON. No protocol overhead, no wire attributes, no object inspector surprises. If something breaks, you know exactly where to look.
| Dependency | Version |
|---|---|
| PHP | ^8.2 |
| Laravel | ^11.0 | ^12.0 |
| Alpine.js | ^3.0 |
| DaisyUI | ^5.0 |
| blade-heroicons | ^2.4 |
composer require edumicro/daisyblade
php artisan daisyblade:installThe install command will ask whether to publish assets for Vite (recommended) or as a public script tag:
# Vite — assets published to resources/js/daisyblade.js
php artisan daisyblade:install --vite
# Script tag — assets published to public/vendor/daisyblade/
php artisan daisyblade:install --public// vite.config.js
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: [laravel({ input: ['resources/js/app.js'] })],
})// resources/js/app.js
import './daisyblade.js'
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()<script src="/vendor/daisyblade/daisyblade.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>All components use the dbl prefix:
<x-dbl::display.badge label="Active" color="success" />
<x-dbl::form.input name="email" type="email" label="Email" />
<x-dbl::display.data-table :load-url="route('products.data')" :columns="$columns" />No JavaScript. Receive props, render HTML.
| Component | Usage |
|---|---|
display.accordion |
DaisyUI accordion (CSS-driven) |
display.avatar |
User avatar with initials fallback |
display.badge |
Status badges with color variants |
display.card |
Content card with optional shadow |
display.chat-bubble |
Chat message bubble |
display.collapse |
DaisyUI collapse panel (CSS-driven) |
display.compare |
Side-by-side property comparison table |
display.diff |
Code diff viewer (line-by-line +/−/~) |
display.hover-3d-card |
CSS 3D tilt card on hover |
display.hover-gallery |
CSS-animated image gallery |
display.kbd |
Keyboard shortcut display |
display.list |
Styled list component |
display.mask |
DaisyUI mask shapes |
display.radial-progress |
Circular progress indicator |
display.resource-details |
Detail view loaded from URL |
display.stat |
KPI card with value, title, trend |
display.status |
Status dot with label |
display.table |
Static HTML table from array data |
display.timeline |
Vertical event timeline |
display.tree |
Recursive tree from nested array |
feedback.alert |
Alert message with type variants |
feedback.loading |
Loading spinner |
feedback.progress |
Linear progress bar |
feedback.skeleton |
Content skeleton placeholder |
feedback.tooltip |
Tooltip wrapper |
form.checkbox |
Checkbox field with label |
form.input |
Text/email/number/date input |
form.radio |
Radio button group |
form.textarea |
Textarea with label |
form.toggle |
Toggle switch |
form.validator |
Inline validation message display |
layout.divider |
Section divider |
layout.footer |
Page footer |
layout.hero |
Hero section |
layout.indicator |
Badge indicator overlay |
layout.join |
DaisyUI join group |
layout.section-wrapper |
Padded section container |
layout.stack |
DaisyUI stack layout |
navigation.breadcrumb |
Breadcrumb trail |
navigation.dock |
Bottom dock navigation |
navigation.steps |
Step indicator |
actions.button |
Button with variants, loading, icon |
actions.fab |
Floating action button |
actions.swap |
Toggle swap element |
components.icon |
Heroicon wrapper |
UI state managed by Alpine. No Axios.
| Component | Usage |
|---|---|
actions.modal |
Modal with Alpine open/close |
display.carousel |
Image/content carousel |
display.filters |
Filter bar with Alpine state |
display.node-graph |
Tree graph with event timeline + detail modal |
display.text-rotate |
Animated rotating text |
feedback.toast |
Toast notification |
form.kv-editor |
Key-value editor with auto-inferring type selector |
form.list-editor |
Chip/tag array-of-strings editor |
form.repeater |
Dynamic repeatable field group |
navigation.menu |
Dropdown / nested menu |
navigation.navbar |
Top navigation bar |
navigation.pagination |
Page navigation controls |
navigation.sidebar |
Collapsible sidebar |
navigation.sidebar-tree |
Nested sidebar menu |
layout.app |
Full page layout with slots |
layout.auth |
Auth page layout |
navigation.tabs |
Tab switcher (inline content) |
Receive a load-url or action prop. Call plain Laravel controllers returning JSON.
| Component | Usage |
|---|---|
display.data-table |
Paginated, sortable, filterable table |
form.filter |
Filter bar for data-table |
form.select |
Select with remote search |
import.spreadsheet |
Excel/CSV chunked import |
sections.auto-form |
Declarative form from schema array |
sections.tabs |
Tabs with lazy-loaded content |
sections.wizard |
Multi-step form with localStorage resume |
<x-dbl::display.badge label="Active" color="success" />
<x-dbl::display.badge label="Pending" color="warning" size="lg" />
<x-dbl::display.badge label="Error" color="error" :outline="true" /><x-dbl::display.stat
title="Monthly revenue"
value="€ 12.400"
description="vs last month"
trend="up"
icon="heroicon-o-banknotes"
/>Renders a tree of simulation/workflow nodes with per-event color coding and a detail modal. Each node is an array with id, parent (null for roots), label, status, events[], and optional meta{}.
@php
$nodes = [
[
'id' => 1,
'parent' => null,
'label' => 'Main trajectory',
'status' => 'completed',
'events' => [
['name' => 'timer_done', 'action_type' => 'terminate', 'termination_type' => 'end', 't_s' => 12.5],
['name' => 'heat_acc', 'action_type' => 'accumulator_abort', 'termination_type' => 'abort', 'acc_value' => 42.7],
],
'meta' => ['h_max_m' => 350.2, 't_final_s' => 12.5],
],
[
'id' => 2,
'parent' => 1,
'label' => 'Branch — ricochet',
'status' => 'failed',
'events' => [
['name' => 'impact', 'action_type' => 'accumulator_fail', 'termination_type' => 'fail', 'acc_value' => 0.1],
],
'meta' => [],
],
];
@endphp
<x-dbl::display.node-graph :nodes="$nodes" label="Execution tree" />Event badge colours: end → green, abort → amber, fail → red, no termination → ghost. Accumulator events show acc_value inline. Click any node dot or event badge to open the detail modal.
{{-- In your Blade view --}}
<x-dbl::display.data-table
:load-url="route('products.data')"
:columns="$columns"
:per-page="15"
:filters-url="route('products.filters')"
/>// In your controller
public function index()
{
return view('products.index', [
'columns' => [
['key' => 'name', 'label' => 'Name', 'sortable' => true],
['key' => 'category', 'label' => 'Category', 'sortable' => false],
['key' => 'price', 'label' => 'Price', 'sortable' => true],
],
]);
}
// Data endpoint — returns JSON
public function data(Request $request)
{
$products = Product::query()
->when($request->search, fn($q) => $q->where('name', 'like', "%{$request->search}%"))
->orderBy($request->sort_by ?? 'name', $request->sort_dir ?? 'asc')
->paginate($request->per_page ?? 15);
return response()->json($products);
}<x-dbl::sections.auto-form
:schema="[
['name' => 'name', 'label' => 'Product name', 'order' => 10],
['name' => 'category_id', 'label' => 'Category', 'type' => 'relation',
'options-url' => route('categories.options'), 'order' => 20],
['name' => 'price', 'label' => 'Price', 'type' => 'money', 'order' => 30],
['name' => 'active', 'label' => 'Active', 'type' => 'toggle', 'order' => 40],
]"
action="{{ route('products.store') }}"
method="POST"
/>// Controller — plain Laravel, no Livewire
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'category_id' => 'required|exists:categories,id',
'price' => 'required|numeric|min:0',
'active' => 'boolean',
]);
$product = Product::create($validated);
// DaisyBlade expects: {success, redirect} or {success: false, errors}
return response()->json([
'success' => true,
'redirect' => route('products.index'),
]);
}<x-dbl::form.repeater
name="events"
label="Events"
:fields="[
['name' => 'label', 'type' => 'text', 'label' => 'Label'],
['name' => 'value', 'type' => 'number', 'label' => 'Value'],
]"
:value="old('events', [])"
add-label="Add event"
:min="1"
:max="10"
/>Renders a table of key/value rows with an auto-inferring type selector (num/str/bool/{…}). Serialises to a hidden <input> as JSON.
<x-dbl::form.kv-editor
name="parameters"
:value="$model->parameters ?? []"
label="Parámetros"
hint="Campos habituales: mass_kg, v0_ms, diameter_m"
/>value accepts a PHP associative array. Types are inferred automatically when the user types; the selector lets them override if needed.
Chip/tag input for an array of strings. Enter or comma adds a chip; Backspace removes the last one.
<x-dbl::form.list-editor
name="output_fields"
:value="$model->output_fields ?? []"
label="Campos de output"
hint="Disponibles: t_s, x_m, h_m, v_ms, mach"
placeholder="Escribe un campo y pulsa Enter…"
/><x-dbl::sections.wizard
form-id="product-onboarding"
schema-version="2"
:user-id="auth()->id()"
action="{{ route('products.store') }}"
:steps="[
['title' => 'Basic info', 'fields' => ['name', 'category_id']],
['title' => 'Pricing', 'fields' => ['price', 'currency']],
['title' => 'Visibility', 'fields' => ['active', 'publish_at']],
]"
/>If the user refreshes mid-wizard, their progress is automatically restored from localStorage. The storage key is versioned (product-onboarding_{userId}_v2), so changing schema-version invalidates stale state.
{{-- resources/views/products/index.blade.php --}}
<x-dbl::layout.app title="Products">
<x-slot:navbar>
<x-dbl::navigation.navbar>
<x-dbl::navigation.breadcrumb :items="[
['label' => 'Dashboard', 'url' => route('dashboard')],
['label' => 'Products'],
]"/>
</x-dbl::navigation.navbar>
</x-slot:navbar>
<x-slot:sidebar>
<x-dbl::navigation.sidebar />
</x-slot:sidebar>
<x-dbl::display.data-table
:load-url="route('products.data')"
:columns="$columns"
/>
</x-dbl::layout.app>All Type 3 components expect controllers to return JSON following this contract:
// Success with redirect
{ "success": true, "redirect": "/products" }
// Success with data (for remote selects, resource-details, etc.)
{ "success": true, "data": [...], "meta": { "current_page": 1, "last_page": 5 } }
// Validation failure
{ "success": false, "errors": { "name": ["The name field is required."] } }Laravel's response()->json() + standard validation exceptions handle this automatically if you let them.
To customise any component, publish the views:
php artisan vendor:publish --tag=daisyblade-viewsPublished views in resources/views/vendor/daisyblade/ take precedence over package views. Edit freely — your customisations survive package updates.
composer test
# or
vendor/bin/pest188 tests, 294 assertions. All green.
MIT — Eduardo de Vicente / Microvalencia Soluciones Informáticas S.L.