Skip to content
LarawellUi

Installation

LarawellUi copies components into your app, then gets out of the way. Everything below takes about five minutes.

Requirements

  • PHP 8.3 or later, and Laravel 12 or 13.
  • Vite and Tailwind CSS 4.1 or later. New Laravel apps have both.
  • Pages rendered with Blade, Livewire components included.
  • The date picker, date range picker, time picker and price roll need PHP's intl extension.

Browsers

  • Everything works as designed in Chrome and Edge 117, Firefox 129 and Safari 17.5 or later, all released in 2024.
  • In Chrome and Edge 114 to 116, Firefox 128 and Safari 17 to 17.4, everything works, but some opening and closing animations are skipped.
  • In anything older, the select, the date pickers, the time picker's list and columns, the phone's country list, the dropdown and the tooltips can't open, because they're built on the browser's own popover. The rest still render, but aren't tested there.
  • On iPhone and iPad before iOS 18.3, tapping outside an open popover doesn't close it, because of a WebKit bug. Choosing an option or pressing its trigger again still closes it.

Content Security Policy

  • The components work under a strict policy such as default-src 'self', with no 'unsafe-inline' for scripts or styles. They render no inline scripts, event handlers or style attributes; sizes worked out on the server are classes.
  • Examples that call a component's JavaScript come with a script for your own JS file, not onclick.
  • The captcha's Turnstile, reCAPTCHA or hCaptcha option loads that provider's script, so allow its domain in script-src and frame-src.

Livewire

  • Livewire isn't needed, but the components work inside Livewire 3 and 4 components, checked in a browser. They keep what the person did through every render (an open modal or menu, a panel they opened, a running stopwatch, files they picked) while their content updates. Each one's page shows how under Usage.
  • Form controls bind with wire:model and need no name. They show the property's value after every render, a value set in PHP included, and its validation errors. The file upload sends files to Livewire's temporary uploads (WithFileUploads).
  • A component shows a toast with $this->dispatch('toast', type: 'success', message: 'Saved.') and opens or closes a modal with 'modal-open' and 'modal-close', each with an id. A flash before a redirect, wire:navigate included, still shows as a toast.
  • Components added to the page later, by a Livewire render, wire:navigate or HTML you fetch, set themselves up.
  • Where the browser owns the state, the server's props only set how it starts: an accordion's open, and a stopwatch or timer, which keeps running and reads its props once. A progress bar you drive with progress.set() goes in a wire:ignore. The captcha's Turnstile, reCAPTCHA and hCaptcha tokens aren't bound by wire:model; use those in a regular form.

When it isn't a fit

  • Projects on Tailwind CSS v3, Bootstrap or another CSS framework: the components are styled with Tailwind v4 classes and theme tokens.
  • Inertia apps whose pages are React or Vue: the components are Blade, so they can't render there.

Start a new app

Starting from scratch? The starter kit is a Laravel 13 app with sign-in, registration, password reset, email verification, account settings and a dashboard, built from these components. It's plain Blade with no React, Vue or Alpine, sends a strict Content Security Policy, and comes with Laravel Boost and the MCP server set up for AI agents. The components are already installed, so everything below applies to it too.

Terminal
composer create-project larawellui/starter-kit my-app
cd my-app
npm install && npm run build
composer run dev

With the Laravel installer, laravel new my-app --using=larawellui/starter-kit does the same first step.

Its source, security notes and checklist for going live are on GitHub. Already have an app? Add components to it below.

Add components

Require the package, then add components by name. Each one brings the components, PHP helpers and validation rules it depends on. Before writing anything it checks package.json, and stops if Tailwind CSS is older than 4.1.

Terminal
composer require --dev larawellui/larawellui
php artisan larawell:add datepicker table
npm run build

Run it again whenever you like, for example after composer update; --installed updates every component you already have. Files you haven't edited get the new version, files you have edited are skipped and listed, and --force overwrites those too. It keeps track in larawellui.lock in your app's root, so commit that file.

Terminal
php artisan larawell:list              # everything you can add
php artisan larawell:add               # pick from a list
php artisan larawell:add --all
php artisan larawell:add select --dry-run  # show what would change

php artisan larawell:add --installed       # update everything you have
php artisan larawell:diff                  # what the skipped files would miss
php artisan larawell:diff select/index.blade.php

Values from your data

A prop that takes one of a set of values (variant, size, tone…) throws on anything else, so a typo shows up while you build instead of shipping the wrong look. A value from your data, such as an order's status or a size from settings, can be one you didn't expect, and then the page fails. Map it to the component's values first, with a default. With an enum, a method on it keeps the mapping in one place.

Blade
<x-widget.table.badge :tone="['paid' => 'success', 'failed' => 'error', 'pending' => 'warning'][$order->status] ?? 'neutral'">
    {{ $order->status }}
</x-widget.table.badge>

What an update can change

What you build on is only ever added to, never renamed or removed: component names, props and the values they take, data-* hooks, JS exports, the commands and their flags, and the config keys. A release can still get stricter about input that never worked, such as a mistyped value that used to be ignored and now throws. The changelog lists every change by component, so check the ones you use before running --installed.

Common tasks

The catalogue is sorted by component; here it's sorted by what you came to do. Each one links to a live example you can copy.

Forms from your validation rules

Already have a backend? Your Form Request, or the array you pass to validate(), already says what each field is. Give each widget the rule's key as its name, and it fills in old input and shows that field's errors by itself. Add required where the rules say required, and leave it off for nullable. In a Livewire component, use wire:model instead of name; for a named error bag, add bag="…".

In your rules Widget Set
string, min:3, max:120 text-input minlength="3" maxlength="120"
string, max:500 (longer text) textarea maxlength="500" counter
email text-input type="email"
url text-input type="url"
integer, min:1, max:12 number :decimals="0" :min="1" :max="12"
numeric, decimal:0,2 number :decimals="2"
boolean checkbox or switch unchecked-value="0"
accepted checkbox required
in:… or Rule::in([…]) select, or radio for a few :options="[…]"
exists:rooms,id select :options="$rooms", searchable or search-url for long lists
array, plus in or exists for each select multiple
date, after:today datepicker :min="…" :max="false" (the latest date is today unless you change it)
date, before_or_equal:today datepicker nothing: today is the latest by default; birthday for an age
date_format:H:i time-picker min, max, step
a start and an end, after_or_equal on the end date-range-picker start-name and end-name: your two field names; :max="false" for future dates, as on the datepicker
file, mimes:pdf,jpg, max:2048 file-upload accept=".pdf,.jpg,.jpeg" max-size="2048" (mimes:jpg takes .jpeg files too)
image file-upload accept="image/*"
confirmed, Password::defaults() password new, with :min and mixed-case, numbers or symbols as your defaults ask, plus a second password named password_confirmation
digits:6, or a one-time code verification-code :length="6"
required_if:contact,phone show-if around the field field="contact" value="phone"
a phone number phone country="…" to start in one

These mirror your rules so people find out early, as they type or pick; your rules on the server still decide. For example, these rules:

app/Http/Requests/StoreBookingRequest.php
'name' => ['required', 'string', 'max:120'],
'email' => ['required', 'email'],
'guests' => ['required', 'integer', 'min:1', 'max:12'],
'room' => ['required', Rule::in(['standard', 'deluxe', 'suite'])],
'arrival' => ['required', 'date', 'after:today'],
'notes' => ['nullable', 'string', 'max:500'],
'newsletter' => ['boolean'],
'passport' => ['nullable', 'file', 'mimes:pdf,jpg,png', 'max:2048'],

become this form. A file field needs enctype="multipart/form-data" on the form.

resources/views/bookings/create.blade.php
<form method="POST" action="{{ route('bookings.store') }}" enctype="multipart/form-data" class="grid gap-5">
    @csrf
    <x-widget.text-input name="name" label="Name" maxlength="120" required />
    <x-widget.text-input name="email" type="email" label="Email" required />
    <x-widget.number name="guests" label="Guests" :decimals="0" :min="1" :max="12" required />
    <x-widget.select name="room" label="Room" :options="['standard' => 'Standard', 'deluxe' => 'Deluxe', 'suite' => 'Suite']" required />
    <x-widget.datepicker name="arrival" label="Arrival" :min="now()->addDay()->toDateString()" :max="false" required />
    <x-widget.textarea name="notes" label="Notes" maxlength="500" counter />
    <x-widget.checkbox name="newsletter" label="Send me offers" unchecked-value="0" />
    <x-widget.file-upload name="passport" label="Passport scan" accept=".pdf,.jpg,.jpeg,.png" max-size="2048" />
    <x-widget.button type="submit">Book</x-widget.button>
</form>

Where files go

What Where
Blade components, used as <x-widget.*> resources/views/components/widget/
JavaScript, imported from resources/js/app.js resources/js/widget/
Theme and base CSS, imported from resources/css/app.css resources/css/widget/
PHP helpers (FormField, ElementIds, and Countries for the phone) App\View\Widget
Validation rules (date rules, Captcha) App\Rules

To use other namespaces or paths, publish the config. Namespaces must sit under a PSR-4 root in your composer.json.

Terminal
php artisan vendor:publish --tag=larawellui-config

Theme

Components only use the colour names in resources/css/widget/theme.css. Change the values there and every component follows.

resources/css/widget/theme.css
@theme {
    --color-primary: #15803d;       /* text, borders, focus rings */
    --color-on-primary: #ffffff;
    --color-foreground: #101828;    /* main text */
    --color-field: #f5f6f8;         /* input backgrounds */
    --color-error: #b42318;
    /* … */
}

Dark mode

The theme comes with a dark set of the same colours, all at WCAG AA. Put class="dark" or data-theme="dark" on <html> and every component follows, popovers and dialogs included. On any other element, only that part of the page does. To follow the device's setting instead, swap the selector in theme.css for @media (prefers-color-scheme: dark) { :root { … } }.

Each colour does one job, which is what lets dark mode stay readable: primary is for text, borders and focus rings, and primary-fill is for solid backgrounds with on-primary text on them. On a dark page a shade light enough to read can't also carry white text, so the two differ there. error-fill and success-fill do the same for red and green. In light mode the fills follow their base colour unless you set them, so changing primary restyles both. In dark mode they're a shade of their own, so change your brand colour there too.

resources/css/widget/theme.css
:where(.dark, [data-theme=dark]) {
    --color-primary: var(--color-green-400);       /* reads on the dark surface */
    --color-primary-fill: var(--color-green-700);  /* carries white text */
    /* … */
}

Validating dates

The date pickers stop people choosing future dates using their own local today. Validate with the rules that come with them, not before_or_equal:today, which uses the server's UTC date and can reject a real local today.

app/Http/Requests/StoreBookingRequest.php
use App\Rules\MinimumAge;
use App\Rules\NotAfterToday;

return [
    'start_date' => ['required', new NotAfterToday],
    'date_of_birth' => ['required', new MinimumAge(18)],
];

Dates in any language

  • Dates are written the way your app's locale writes them: Sep 26, 2026 in en, 26 Sept 2026 in en_GB, 2026年9月26日 in ja. Month and weekday names, and digits, follow too. What's submitted is always Y-m-d.
  • The week starts on the locale's first day: Monday in most of the world, Sunday in the US, Saturday in parts of the Middle East. week-start overrides it, and locale sets a different locale for one picker.
  • For Arabic, Hebrew, Persian and Urdu, set dir="rtl" on <html>: the calendar, its arrows and the arrow keys mirror.
  • Formatting needs PHP's intl extension; without it, dates show as 2026-09-26.

The pickers' own text ("Choose a date", "Clear", "Previous month" and so on) is plain English in the component files, like every other component's. It's your copy, so change it there, or wrap it in __() if your app is multilingual.

For AI agents

Point your agent at /llms.txt. From there it can read any component's entry and either run larawell:add or write the files in itself. For an app built with Livewire, each entry's usage includes a Livewire example.

In your own app, php artisan larawell:mcp is an MCP server the agent can connect to. On top of the catalogue, it knows which components this app has, which installed files are out of date or edited, and it can install: a dry run first, which writes nothing until you agree, and never over files you edited.

Terminal, from your app's root
claude mcp add larawellui -- php artisan larawell:mcp

Claude Desktop, Windsurf, Zed, JetBrains and any other client: the command php, with the arguments /path/to/your-app/artisan larawell:mcp. The full path to artisan is what makes it work where a client may not start the server in your app's folder. If a desktop app can't find php, give it the full path too (which php).

/llms.txt
An index of every component, for agents that follow the llms.txt convention.
/r/index.json
Every component, with a link to its full entry.
/r/datepicker.json
One component's full entry: install command, props, examples and source.
php artisan larawell:list --json
The same catalogue offline, from the installed package.
php artisan larawell:mcp
An MCP server in your app: the catalogue, what this app has installed and edited, and installing with a dry run first.