blatui
Get started

Installation

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.

Laravel 11+ PHP 8.2+ Tailwind CSS v4 Alpine.js 3 Node 18+

TL;DR — already on Laravel + Tailwind v4?

Install, publish the foundations, wire two entrypoints, then start building.

Terminal
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):

resources/css/app.css
@import "./blatui.css";
resources/js/app.js
import "./blatui.js";

Verify, then add your first components:

Terminal
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:

Terminal
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):

resources/js/app.js
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();
Using an AI editor? Connect the BlatUI MCP server so your agent can search, read & install components — plus Laravel Boost integration.

Step-by-step

1

Install the package

Pull BlatUI in via Composer. It ships the Artisan commands and the component registry.

Terminal
composer require anousss007/blatui
2

Install the peer dependencies

Two Composer packages power the components — twMerge() (the cn() equivalent) and Lucide icons:

Terminal
composer require gehrisandro/tailwind-merge-laravel mallardduck/blade-lucide-icons

Then the front-end packages — Alpine, its plugins, and ApexCharts (for the charts):

Terminal
npm install -D alpinejs @alpinejs/anchor @alpinejs/collapse @alpinejs/focus apexcharts
3

Publish & import the foundations

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.

Terminal
php artisan vendor:publish --tag=blatui-foundations

Then point your two Vite entrypoints at them — replace the contents of each file:

resources/css/app.css
@import "./blatui.css";
resources/js/app.js
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.
4

Verify your setup

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.

Terminal
php artisan blatui:init
5

Add components

Copy components — and their dependencies — straight into resources/views/components/ui. They're yours now: edit freely.

Terminal
php artisan blatui:add button card input

# browse everything that's available
php artisan blatui:list
6

Use them

Every component is a Blade tag under the ui namespace. Compose away:

resources/views/welcome.blade.php
<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

Welcome back
Sign in to continue.

Installing into an existing project

Already have a Laravel app with Tailwind set up? Everything is additive — you don't replace your files.

Tailwind v4 is required. BlatUI relies on v4-only features (@theme inline, oklch tokens). If you're still on Tailwind v3, migrate first:
Terminal
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.

resources/css/app.css
@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:

resources/js/app.js
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:

resources/js/app.js
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.

Keeping components up to date

You own the copied files, so nothing updates behind your back. blatui:update compares what you have against the version this release ships, shows the diff, and lets you decide — file by file.

Requires BlatUI 1.21 or newer. On an older install the command isn't registered (Command "blatui:update" is not defined) — run composer update anousss007/blatui first.

See what changed first

A dry run writes nothing. Add --diff for the full unified diff of every file that differs.

Terminal
php artisan blatui:update --dry-run --diff

Then update

Without arguments it walks every installed family; pass names to scope it. Each differing file is confirmed individually.

Terminal
php artisan blatui:update

# or just these, without prompting
php artisan blatui:update button card --force

Running a formatter over your views?

Pint or Prettier over resources/views rewrites the layout of every component you copied, so all of them read as changed. They're counted separately as same content, reformatted, and this leaves them out:

Terminal
php artisan blatui:update --dry-run --ignore-whitespace
A file that differs is either your customisation or an outdated copy — nothing on disk tells them apart, so BlatUI never overwrites one silently. Even --force keeps your version next to it as .bak (opt out with --no-backup). Files a family gained in a newer release are simply added.
This covers the component stubs. The CSS/JS foundations are published rather than copied — re-sync them with php artisan vendor:publish --tag=blatui-foundations --force. blatui:init tells you when the engine has fallen behind what your components expect.

Localization

Every string a component shows or announces (button text, aria-labels, placeholders, empty states, screen-reader text) goes through Laravel’s __(), with the English text as the key. Translate them in a JSON file per locale. Strings that carry a value use a placeholder, so the value can move.

lang/es.json
{
    "Search...": "Buscar...",
    "No results.": "Sin resultados.",
    "Mark all read": "Marcar todo como leído",
    "You're all caught up": "Todo al día",
    "Remove :name": "Quitar :name",
    ":selected of :total row(s) selected.": ":selected de :total fila(s) seleccionadas."
}

A key with no translation falls back to the English, so you can translate as you go. Any label you pass as a prop (placeholder, empty-text, label…) is used as given.

Performance

A screen built from BlatUI renders hundreds of anonymous components, and on a busy page Blade's per-component overhead is most of the render time. Two changes cut it down without touching a component.

Compile the components with Blaze

Blaze compiles anonymous components into plain PHP functions. It works with or without Livewire. Every component in components/ui renders the same HTML under Blaze as under Blade, and the Livewire morph suite passes with it on. Rendering all of the docs examples takes about half as long.

Terminal
composer require livewire/blaze:^1.0
app/Providers/AppServiceProvider.php
use Livewire\Blaze\Blaze;

public function boot(): void
{
    Blaze::optimize()->in(resource_path('views/components/ui'));
}

Then run php artisan view:clear. Use the default compiler only: the fold and memo strategies bake a component's output in, and many BlatUI components read state at render time.

Render the components with their tag (<x-ui.card>), never with @include('components.ui.…') or view(): a file Blaze compiled renders nothing that way. When you edit your copies, keep to Blaze's limitations. Shared view data such as $errors is not injected into a Blaze component, so pass it in as a prop.

Memoize the tailwind-merge cache

Every $attributes->twMerge() looks its result up in your default cache store. On Redis that is a network round trip per component, over a thousand on a large page. A memoized store (Laravel 12.9+) answers repeats from memory for the rest of the request:

app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Cache;
use TailwindMerge\Contracts\TailwindMergeContract;
use TailwindMerge\TailwindMerge;

public function register(): void
{
    $this->app->singleton(TailwindMergeContract::class, fn () => TailwindMerge::factory()
        ->withConfiguration(config('tailwind-merge', []))
        ->withCache(Cache::memo())
        ->make());
}

Server-rendered forms & the foundations utilities

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
resources/views/components/has-field.blade.php
<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).