Tune it live. Every preset is pure CSS variables.
"Default" follows the body font.
Copy your complete theme and paste it as your resources/css/app.css — the Tailwind import, every token and the @theme mapping are all included, so it works as-is.
Add BlatUI to any Laravel app in a few minutes. You install the foundations once, then pull in components on demand — and you own every line.
Install, publish the foundations, wire two entrypoints, then start building.
composer require anousss007/blatui gehrisandro/tailwind-merge-laravel mallardduck/blade-lucide-icons
npm install -D alpinejs @alpinejs/anchor @alpinejs/collapse @alpinejs/focus
php artisan vendor:publish --tag=blatui-foundations
Point your two Vite entrypoints at the published foundations (replace each file's contents):
@import "./blatui.css";
import "./blatui.js";
Verify, then add your first components:
php artisan blatui:init
php artisan blatui:add button card input
Charts are opt-in. ApexCharts (~140kb) isn't installed by default — only when you add a chart:
php artisan blatui:add chart
npm install -D apexcharts
php artisan vendor:publish --tag=blatui-charts
Then wire it in app.js (swap the one-line bootstrap for the explicit form):
import Alpine from 'alpinejs';
import { registerBlatUI } from './blatui-core.js';
import { registerCharts } from './blatui-charts.js';
registerBlatUI(Alpine);
registerCharts(Alpine);
window.Alpine = Alpine;
Alpine.start();
Pull BlatUI in via Composer. It ships the Artisan commands and the component registry.
composer require anousss007/blatui
Two Composer packages power the components — twMerge() (the cn() equivalent) and Lucide icons:
composer require gehrisandro/tailwind-merge-laravel mallardduck/blade-lucide-icons
Then the front-end packages — Alpine, its plugins, and ApexCharts (for the charts):
npm install -D alpinejs @alpinejs/anchor @alpinejs/collapse @alpinejs/focus apexcharts
Publish the theme CSS and the Alpine/chart/calendar engine. They land in resources/css/blatui.css and resources/js/blatui.js — and they're yours to edit.
php artisan vendor:publish --tag=blatui-foundations
Then point your two Vite entrypoints at them — replace the contents of each file:
@import "./blatui.css";
import "./blatui.js";
blatui.css bundles Tailwind, the design tokens and the @theme mapping; blatui.js boots Alpine + its plugins and lazy-loads ApexCharts. Want a custom palette? Open Customize (top-right), tune it, and hit Copy theme CSS for a complete, ready-to-paste app.css.
Run the doctor. It checks every package, the theme tokens and the Alpine bootstrap — and that the foundations are actually imported — then tells you exactly what's missing.
php artisan blatui:init
Copy components — and their dependencies — straight into resources/views/components/ui. They're yours now: edit freely.
php artisan blatui:add button card input
# browse everything that's available
php artisan blatui:list
Every component is a Blade tag under the ui namespace. Compose away:
<x-ui.card class="max-w-sm">
<x-ui.card-header>
<x-ui.card-title>Welcome back</x-ui.card-title>
<x-ui.card-description>Sign in to continue.</x-ui.card-description>
</x-ui.card-header>
<x-ui.card-content class="space-y-3">
<x-ui.input type="email" placeholder="[email protected]" />
<x-ui.button class="w-full">Sign in</x-ui.button>
</x-ui.card-content>
</x-ui.card>
Renders
Already have a Laravel app with Tailwind set up? Everything is additive — you don't replace your files.
@theme inline, oklch tokens). If you're still on Tailwind v3, migrate first:
npx @tailwindcss/upgrade
CSS — add, don't replace
Append the import to your existing app.css (below your @import "tailwindcss"). The foundations layer on top of your own styles.
@import "tailwindcss";
/* …your existing styles… */
@import "./blatui.css";
JS — already running Alpine?
Don't import blatui.js (it would boot a second Alpine). Register BlatUI into your Alpine instance instead, before you start it:
import Alpine from 'alpinejs'
import { registerBlatUI } from './blatui-core.js'
registerBlatUI(Alpine) // plugins + theme store + chart/calendar engines
window.Alpine = Alpine
Alpine.start()
JS — using Livewire or Flux?
Livewire bundles and starts Alpine for you, so don't npm install alpinejs (that's the duplicate-Alpine trap) and don't import blatui.js. Install only the plugins — npm install -D @alpinejs/anchor @alpinejs/collapse @alpinejs/focus (add apexcharts only if you use charts) — and register BlatUI onto Livewire's Alpine via the alpine:init hook:
import { registerBlatUI } from './blatui-core.js'
document.addEventListener('alpine:init', () => {
registerBlatUI(window.Alpine)
})
BlatUI registers a theme store for dark mode. If Flux already drives dark mode, pass registerBlatUI(window.Alpine, { darkMode: false }) so the two don't both toggle .dark.
Building a reusable form-field DX layer (a wrapper that re-emits {{ $slot }})? Reach for the foundations utilities on raw elements rather than nesting <x-ui.*> form controls inside that slot.
Footgun: <x-ui.*> as slot content of an @aware anonymous component may not compile.
When a <x-ui.input/.textarea/.select> is passed as the slot content of an anonymous wrapper that re-wraps the slot through another anonymous component using @aware, the inner tag is left literal in the output. A content component leaves an orphan @endif → ParseError (500); a self-closing one renders an unknown <x-ui.input> element that produces no <input> at all — the field is silently absent.
⚠️ This passes every render test. A GET → assertOk() still returns 200, and POST-based feature tests still pass — only a browser test that actually fills the field catches it. (Direct, un-wrapped use compiles fine; the bug only appears through the @aware slot layer.)
The fix: in any DX layer that re-wraps a slot, render raw elements styled by the foundations utilities — zero <x-ui.*> in the slot, and it's just as “native”. The utilities are the twin of the components' class-strings, shipped in blatui.css:
.blat-input
.blat-textarea
.blat-select
.blat-checkbox
.blat-radio
.blat-label
<label class="blat-label">{{ $label }}</label>
<input name="{{ $name }}" class="blat-input" />
<textarea name="{{ $name }}" class="blat-textarea"></textarea>
<select name="{{ $name }}" class="blat-select">...</select>
php artisan blatui:doctor flags this for you — it scans your compiled views for any literal <x-ui.*> tag that leaked into the HTML (a tag that failed to compile).