leenuxus / jarenui
A Jaren-UI compatible Livewire component library for Laravel — 50+ components, dark mode, CSS-variable theming.
Requires
- php: ^8.4
- blade-ui-kit/blade-heroicons: ^2.0
- illuminate/support: ^13.0
- illuminate/view: ^13.0
- livewire/livewire: ^4.0
Requires (Dev)
- laravel/pint: ^1.0
README
50+ production-ready Livewire components — dark mode, CSS-variable theming, Alpine.js interactivity, full ARIA accessibility, and zero Tailwind config required.
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.4 |
| Laravel | ^13 |
| Livewire | ^4.0 |
| Alpine.js | ^3.0 |
| Tailwind CSS | ^4.0 (optional — all styling uses CSS variables) |
Installation
composer require leenuxus/jarenui php artisan jaren:install
That's it. The installer:
- Publishes
config/jarenui.php - Injects
@jarenStylesinto your layout<head> - Injects
<livewire:jaren.toast/>before your</body>
Manual setup
If you prefer not to use the installer:
{{-- In your layout <head> --}} @jarenStyles {{-- Before </body> --}} <livewire:jaren.toast/>
Alpine.js
JarenUI uses Alpine.js for interactivity. Load it in your layout or app.js:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
Quick start
{{-- Button --}} <x-jaren::button>Save changes</x-jaren::button> <x-jaren::button variant="danger" icon="trash" wire:click="delete">Delete</x-jaren::button> {{-- Input --}} <x-jaren::input label="Email" icon="envelope" wire:model="email"/> {{-- Select (searchable) --}} <x-jaren::select label="Role" :options="$roles" searchable wire:model="role"/> {{-- Accordion --}} <x-jaren::accordion> <x-jaren::accordion.item title="What is jarenui?"> A Livewire component library… </x-jaren::accordion.item> </x-jaren::accordion> {{-- Tabs --}} <x-jaren::tabs default="overview"> <x-jaren::tabs.tab name="overview">Overview</x-jaren::tabs.tab> <x-jaren::tabs.tab name="settings">Settings</x-jaren::tabs.tab> <x-jaren::tabs.panel name="overview">Overview content…</x-jaren::tabs.panel> <x-jaren::tabs.panel name="settings">Settings content…</x-jaren::tabs.panel> </x-jaren::tabs> {{-- Table (extend the base class) --}} <livewire:users-table/> {{-- Kanban --}} <livewire:project-kanban/> {{-- Toast from Livewire PHP --}} $this->dispatch('jaren-toast', type: 'success', title: 'Saved!');
Artisan commands
| Command | Description |
|---|---|
php artisan jaren:install |
Install jarenui (publish assets, inject into layout) |
php artisan jaren:publish --views |
Publish Blade views for customisation |
php artisan jaren:publish --config |
Publish config file |
php artisan jaren:publish --assets |
Publish CSS to public/vendor/jarenui/ |
php artisan jaren:publish --stubs |
Publish layout stub |
php artisan jaren:make-table UsersTable --model=User |
Generate a Table component |
php artisan jaren:make-kanban ProjectKanban |
Generate a Kanban component |
Theming
All visual tokens are CSS custom properties. Override any in your own CSS — no Tailwind config or build step needed:
/* resources/css/app.css */ :root { --accent: #7c3aed; /* Brand primary */ --accent-bg: #f5f3ff; --accent-text: #6d28d9; --radius: 8px; /* Corner radius */ }
Or via .env / config/jarenui.php:
jarenui_ACCENT=#7c3aed
jarenui_THEME=violet
Built-in themes
Add a class to <html> to activate a named theme:
| Class | Colour |
|---|---|
theme-rose |
Rose / pink |
theme-violet |
Violet / purple |
theme-emerald |
Emerald green |
theme-amber |
Amber / gold |
theme-sharp |
Reduced border radii |
theme-rounded |
Increased border radii |
Dark mode
Set data-theme="dark" on <html> (or use Tailwind's .dark class):
<html data-theme="{{ auth()->user()?->dark_mode ? 'dark' : 'light' }}">
Toggle dynamically with Alpine:
<button @click=" const d = document.documentElement; d.setAttribute('data-theme', d.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'); ">Toggle dark</button>
Component reference
Layouts
<x-jaren::header> — Sticky app header with brand, nav, search, and actions slots.
<x-jaren::sidebar> — Collapsible sidebar with sections, items, and user footer.
<x-jaren::navbar> — Standalone horizontal nav bar.
Primitives
<x-jaren::button>
<x-jaren::button variant="primary|secondary|ghost|danger|success|warning" size="sm|md|lg" icon="HEROICON" icon-end="HEROICON" :loading="bool" :disabled="bool" href="URL"> Label </x-jaren::button>
<x-jaren::badge>
<x-jaren::badge color="blue|green|red|yellow|gray|purple|pink" dot close>Text</x-jaren::badge>
<x-jaren::avatar>
<x-jaren::avatar name="Jane Doe" src="/photo.jpg" size="xs|sm|md|lg|xl" color="blue|auto" status="online|away|busy|offline" shape="circle|square"/>
<x-jaren::brand>
<x-jaren::brand name="Acme" src="/logo.svg" href="/" dot badge="Beta"/>
Form
<x-jaren::input>
<x-jaren::input label="Name" hint="Your full name" error="Required" icon="user" icon-end="check" prefix="$" suffix=".com" clearable copyable size="sm|md|lg" wire:model="name"/>
Password with toggle
<x-jaren::input name="password" type="password" label="Password" icon="lock-closed" toggleable size="md" :error="$errors->first('password')" required />
Note:
toggleableoccupies the trailing addon slot. Do not combine it withclearable,copyable,icon-end, orsuffixon the same input.
<x-jaren::textarea>
<x-jaren::textarea label="Bio" :rows="4" resize="none|vertical|both" :auto-resize="true" :max-length="500" show-count wire:model="bio"/>
<x-jaren::select>
<x-jaren::select label="Role" :options="['dev'=>'Developer']" searchable clearable wire:model="role"/>
<x-jaren::checkbox>
<x-jaren::checkbox label="Accept terms" description="You agree to our ToS" :indeterminate="bool" card wire:model="terms"/>
<x-jaren::radio-group> + <x-jaren::radio>
<x-jaren::radio-group label="Plan" variant="card" wire:model="plan"> <x-jaren::radio value="free" label="Free" description="Up to 3 projects" price="$0"/> <x-jaren::radio value="pro" label="Pro" description="Unlimited" price="$12/mo" badge="Popular"/> </x-jaren::radio-group>
<x-jaren::switch>
<x-jaren::switch label="Dark mode" description="Switch theme" align="left|right" card wire:model="dark"/>
<x-jaren::slider>
<x-jaren::slider label="Volume" :min="0" :max="100" :step="1" prefix="" suffix="%" wire:model="volume"/>
<x-jaren::otp-input>
<x-jaren::otp-input :digits="6" :separator="3" wire:model="code"/>
<x-jaren::pillbox>
<x-jaren::pillbox label="Tags" :suggestions="['Laravel','Vue','React']" :max-tags="5" wire:model="tags"/>
<x-jaren::field> — wraps any control with label + hint + error
<x-jaren::field label="Date" :error="$errors->first('date')" hint="YYYY-MM-DD"> <input type="date" wire:model="date" class="inp"> </x-jaren::field>
Navigation
<x-jaren::accordion>
<x-jaren::accordion :multiple="false" flush divided> <x-jaren::accordion.item title="Question" :open="true" icon="question-mark-circle"> Answer text… </x-jaren::accordion.item> </x-jaren::accordion>
<x-jaren::breadcrumbs>
<x-jaren::breadcrumbs :items="[['label'=>'Home','href'=>'/'],['label'=>'Current']]" separator="slash|chevron|dot" home-icon/>
<x-jaren::dropdown>
<x-jaren::dropdown align="left|right|center" position="bottom|top"> <x-slot:trigger><x-jaren::button>Menu</x-jaren::button></x-slot:trigger> <x-jaren::dropdown.item icon="user" href="/profile" kbd="⌘P">Profile</x-jaren::dropdown.item> <x-jaren::dropdown.separator/> <x-jaren::dropdown.item variant="danger">Delete</x-jaren::dropdown.item> </x-jaren::dropdown>
<x-jaren::tabs>
<x-jaren::tabs default="tab1" variant="line|pill|box" :full="false"> <x-jaren::tabs.tab name="tab1" icon="home" badge="3">Overview</x-jaren::tabs.tab> <x-jaren::tabs.panel name="tab1" class="pt-4">Content…</x-jaren::tabs.panel> </x-jaren::tabs>
<x-jaren::pagination>
{{-- With Livewire paginator: --}} <x-jaren::pagination :paginator="$users"/> {{-- Manual: --}} <x-jaren::pagination :current="3" :total="250" :per-page="10" simple size="sm|md"/>
Overlay
<x-jaren::modal>
<x-jaren::modal name="confirm" max-width="sm|md|lg|xl|2xl" danger :closeable="true"> <x-slot:title>Delete?</x-slot:title> <x-slot:description>This cannot be undone.</x-slot:description> Body content… <x-slot:footer> <x-jaren::button variant="secondary" @click="$dispatch('close-modal','confirm')">Cancel</x-jaren::button> <x-jaren::button variant="danger" wire:click="delete">Delete</x-jaren::button> </x-slot:footer> </x-jaren::modal>
Open from PHP: $this->dispatch('open-modal', name: 'confirm')
Open from JS: $dispatch('open-modal', 'confirm')
<x-jaren::tooltip>
<x-jaren::tooltip content="Copy to clipboard" position="top|bottom|left|right" :delay="300"> <x-jaren::button icon="clipboard"/> </x-jaren::tooltip>
<x-jaren::popover>
<x-jaren::popover position="bottom" align="start|center|end" width="260px"> <x-slot:trigger><x-jaren::button>Open</x-jaren::button></x-slot:trigger> <x-slot:content>Popover content here…</x-slot:content> </x-jaren::popover>
Feedback
<x-jaren::toast> — Driven by the jaren.toast Livewire component.
// From Livewire: $this->dispatch('jaren-toast', type: 'success', title: 'Saved!', message: 'All good.', duration: 4000); // Using HasToast trait: use jarenui\Concerns\HasToast; $this->toast()->success('Saved!', 'Changes applied.'); $this->toast()->persistent()->action('Undo', 'undo-delete')->danger('Deleted', 'Row removed.');
<x-jaren::callout>
<x-jaren::callout type="info|success|warning|danger" title="Heads up" dismissible> Body text here. </x-jaren::callout>
<x-jaren::progress>
<x-jaren::progress :value="68" :max="100" label="Storage" show-value color="blue|green|red|yellow" size="xs|sm|md|lg"/> <x-jaren::progress :value="68" circular :radius="24" show-value/>
<x-jaren::skeleton>
<x-jaren::skeleton variant="text|avatar|card|table|form" :lines="3" :rows="4"/>
Display
<x-jaren::card>
<x-jaren::card title="Revenue" description="This month" padding="sm|md|lg" hover shadow> <x-slot:footer> <x-jaren::button size="sm">View all</x-jaren::button> </x-slot:footer> $42,810 </x-jaren::card>
<x-jaren::profile>
<x-jaren::profile name="Jane Doe" handle="janedoe" location="Manila, PH" bio="Laravel developer." :stats="[['value'=>'142','label'=>'Projects']]" :tags="['Laravel','PHP']" verified compact/>
<x-jaren::timeline>
<x-jaren::timeline> <x-jaren::timeline.item title="Deployed" time="2m ago" status="done" icon="check"/> <x-jaren::timeline.item title="Running tests" time="Now" status="active"> 24 tests passing… </x-jaren::timeline.item> <x-jaren::timeline.item title="Notify team" status="pending"/> </x-jaren::timeline>
Full-stack Livewire components
Table — extend jarenui\Livewire\Table:
php artisan jaren:make-table UsersTable --model=User
<livewire:users-table/>
Kanban — extend jarenui\Livewire\Kanban:
php artisan jaren:make-kanban ProjectKanban
<livewire:project-kanban/>
Calendar
A full-featured calendar component for date selection. Supports single dates, multiple dates, and date ranges.
Basic usage
<x-jaren::calendar /> {{-- With initial value --}} <x-jaren::calendar value="2026-05-29" /> {{-- Bound to Livewire --}} <x-jaren::calendar wire:model="date" />
Multiple dates
<x-jaren::calendar multiple wire:model="dates" />
public array $dates = [];
Date range
<x-jaren::calendar mode="range" wire:model="range" />
use JarenUI\DateRange; public ?DateRange $range; public function mount(): void { $this->range = new DateRange(now(), now()->addDays(7)); }
Props
| Prop | Type / Values | Default |
|---|---|---|
wire:model |
Livewire property binding | — |
value |
Y-m-d / Y-m-d,Y-m-d / Y-m-d/Y-m-d |
— |
mode |
single multiple range |
single |
min |
Y-m-d or 'today' |
— |
max |
Y-m-d or 'today' |
— |
unavailable |
comma-separated Y-m-d list |
— |
size |
xs sm base lg xl 2xl |
base |
months |
integer | 1 (2 for range) |
min-range |
integer (days) | — |
max-range |
integer (days) | — |
start-day |
0–6 (0 = Sunday) |
user locale |
with-today |
bool | false |
selectable-header |
bool — click month/year to jump | false |
fixed-weeks |
bool — always show 6 rows | false |
week-numbers |
bool | false |
open-to |
Y-m-d |
— |
force-open-to |
bool | false |
static |
bool — display only, no interaction | false |
navigation |
bool — show prev/next buttons | true |
locale |
BCP-47 string e.g. fr, ja-JP |
browser |
DateRange object
use JarenUI\DateRange; $range = new DateRange(now()->subDays(6), now()); $range->start(); // Carbon — start date $range->end(); // Carbon — end date $range->length(); // int — number of days inclusive $range->contains($date); // bool $range->toArray(); // Carbon[] — one per day (string) $range; // '2026-05-22/2026-05-29' // With Eloquent: Order::whereBetween('created_at', $range)->get();
Persist in the session automatically:
use Livewire\Attributes\Session; #[Session] public ?DateRange $range;
Events
The calendar dispatches an jaren-calendar-change Alpine event whenever the selection changes:
document.addEventListener('jaren-calendar-change', (e) => { console.log(e.detail.value); // 'Y-m-d' | string[] | {start, end} });
Event Calendar
A full-featured Livewire calendar with month, week, and day views for displaying and managing events.
Quick start
Generate a calendar component:
php artisan jaren:make-event-calendar MeetingsCalendar --model=Meeting
Use in Blade:
<livewire:jaren.meetings-calendar />
Static events (no database)
@php use JarenUI\CalendarEvent; $events = [ new CalendarEvent( id: 1, title: 'Team standup', start: '2026-05-30 09:00', end: '2026-05-30 09:30', color: 'blue', description: 'Daily sync', ), ]; @endphp <livewire:jaren.event-calendar :events="$events" />
Loading from a database
Override fetchEvents() in your subclass. It receives the visible date window as two Carbon instances:
class MeetingsCalendar extends \JarenUI\Livewire\EventCalendar { public function fetchEvents(Carbon $from, Carbon $to): array { return CalendarEvent::fromCollection( Meeting::whereBetween('starts_at', [$from, $to])->get(), startKey: 'starts_at', endKey: 'ends_at', titleKey: 'title', colorKey: 'category_color', descriptionKey: 'notes', ); } }
fetchEvents() is called automatically whenever the view or visible period changes.
CalendarEvent
use JarenUI\CalendarEvent; // Construct directly $event = new CalendarEvent( id: 1, title: 'Sprint planning', start: '2026-05-30 10:00', end: '2026-05-30 12:00', color: 'green', // blue|green|amber|red|purple|teal|pink|coral|gray description: 'Plan Q3 sprint backlog', url: 'https://notion.so/sprint-doc', allDay: false, meta: ['room' => 'Conf room A'], ); // Cast from an Eloquent model $event = CalendarEvent::from($meeting, startKey: 'starts_at', endKey: 'ends_at', ); // Cast from a collection $events = CalendarEvent::fromCollection( Meeting::inMonth(2026, 5)->get(), startKey: 'starts_at', endKey: 'ends_at', ); // Accessors $event->date(); // '2026-05-30' $event->startTime(); // '10:00' $event->endTime(); // '12:00' $event->durationMinutes(); // 120 $event->spansMultipleDays();// false $event->toArray(); // array for wire:model / JSON
Component props
| Prop | Type / Values | Default |
|---|---|---|
events |
CalendarEvent[] or plain arrays |
[] |
view |
month week day |
month |
show-toolbar |
bool | true |
show-detail |
bool — event detail panel | true |
creatable |
bool — click empty date to create | false |
start-day |
0–6 (0 = Sunday, 1 = Monday) |
0 |
locale |
BCP-47 string e.g. fr, ja-JP |
en |
available-views |
array of view names | all three |
day-start-hour |
integer | 7 |
day-end-hour |
integer | 20 |
Override in subclass
class MeetingsCalendar extends \JarenUI\Livewire\EventCalendar { public array $availableViews = ['month', 'week']; // hide day view public bool $creatable = true; public int $startDay = 1; // Monday public string $view = 'week'; // default to week view public int $dayStartHour = 8; public int $dayEndHour = 18; public function fetchEvents(Carbon $from, Carbon $to): array { ... } }
Events dispatched
| Event | Payload | When |
|---|---|---|
jaren-event-selected |
{event: array} |
User clicks an event |
jaren-event-created |
{date: 'Y-m-d'} |
User clicks empty date (creatable) |
jaren-event-moved |
{id, date, start, end} |
Drag-and-drop (frontend) |
jaren-view-changed |
{view, year, month} |
View or period changes |
jaren-date-clicked |
{date: 'Y-m-d'} |
Any date click |
Listen in Livewire:
#[On('jaren-event-selected')] public function onEventSelected(array $event): void { $this->selectedId = $event['id']; } #[On('jaren-event-created')] public function onEventCreated(string $date): void { $this->dispatch('open-modal', name: 'create-event', date: $date); }
Event colours
| Value | Appearance |
|---|---|
blue |
Blue (default) |
green |
Green |
amber |
Amber / gold |
red |
Red |
purple |
Purple |
teal |
Teal |
pink |
Pink |
coral |
Coral / orange |
gray |
Neutral gray |
Wizard
A multi-step form component with a progress stepper, per-step validation, and built-in navigation.
Generate a wizard
php artisan jaren:make-wizard OnboardingWizard --steps=account,plan,features,review
This creates:
app/Livewire/OnboardingWizard.php— the PHP classresources/views/livewire/onboarding-wizard/account.blade.phpresources/views/livewire/onboarding-wizard/plan.blade.phpresources/views/livewire/onboarding-wizard/features.blade.phpresources/views/livewire/onboarding-wizard/review.blade.php
Use in Blade:
<livewire:jaren.onboarding-wizard/>
Anatomy
class OnboardingWizard extends \JarenUI\Livewire\Wizard { // Step definitions — id + label (+ optional icon) public array $steps = [ ['id' => 'account', 'label' => 'Account'], ['id' => 'plan', 'label' => 'Plan'], ['id' => 'review', 'label' => 'Review'], ]; // One property bag per step public array $account = ['name' => '', 'email' => '']; public array $plan = ['plan_id' => null]; // Per-step validation rules protected array $stepRules = [ 'account' => [ 'account.name' => 'required|string|max:100', 'account.email' => 'required|email', ], 'plan' => [ 'plan.plan_id' => 'required', ], ]; // Render each step from a Blade partial public function renderAccount(): string { return view('livewire.onboarding-wizard.account', [ 'data' => $this->account, ])->render(); } public function renderPlan(): string { return view('livewire.onboarding-wizard.plan', [ 'data' => $this->plan, ])->render(); } // Called when Next is pressed on the last step public function submit(): void { User::create($this->account); Subscription::create(['user_id' => auth()->id(), ...$this->plan]); $this->complete(); // marks wizard as done, shows success panel } // Data attached to the jaren-wizard-completed event protected function completedData(): array { return ['account' => $this->account, 'plan' => $this->plan]; } }
Component props
| Prop | Type / Values | Default |
|---|---|---|
steps |
array — step definitions |
[] |
variant |
default numbered minimal |
default |
size |
sm md lg |
md |
show-icons |
bool — use icons instead of nums | false |
clickable |
bool — click past steps to jump | true |
show-progress |
bool — linear progress bar | false |
Stepper variants
| Variant | Appearance |
|---|---|
default |
Numbered dots with labels, connecting line, green when done |
numbered |
Same as default |
minimal |
Small dot pills — active dot expands to a pill |
<livewire:jaren.onboarding-wizard variant="minimal"/> <livewire:jaren.onboarding-wizard variant="default" show-progress/>
Hooks
// Called when about to leave a step — useful for cleanup protected function onStepLeaving(string $stepId): void { if ($stepId === 'payment') { // release any held resources } } // Called just after entering a step — useful for loading data protected function onStepEntering(string $stepId): void { if ($stepId === 'review') { $this->summary = $this->buildSummary(); } } // Called when cancel() is triggered protected function onCancel(): void { session()->forget('wizard_progress'); }
Events dispatched
| Event | Payload | When |
|---|---|---|
jaren-wizard-step-changed |
{step: string, index: int} |
Any step navigation |
jaren-wizard-completed |
{data: array} |
complete() is called |
jaren-wizard-cancelled |
— | cancel() is called |
Listen in another Livewire component:
#[On('jaren-wizard-completed')] public function onWizardDone(array $data): void { $this->redirect(route('dashboard')); }
Custom complete panel
Pass a $complete named slot to replace the default success screen:
<livewire:jaren.onboarding-wizard> <x-slot:complete> <div class="text-center py-6"> <h2 class="text-xl font-medium">Welcome aboard!</h2> <p class="mt-2 text-[var(--text2)]">Your account is ready.</p> <a href="{{ route('dashboard') }}" class="mt-4 inline-block ..."> Go to dashboard → </a> </div> </x-slot:complete> </livewire:jaren.onboarding-wizard>
Navigation methods (callable from Blade)
<button wire:click="next">Continue</button> <button wire:click="previous">Back</button> <button wire:click="goToStep(0)">Jump to step 1</button> <button wire:click="cancel">Cancel</button>
Combobox
A versatile combobox that handles basic autocomplete, multi-select, grouped options, async server-side search, and creatable options — all in one component.
Basic autocomplete
<x-jaren::combobox label="Framework" placeholder="Select a framework…" wire:model="framework" :options="['laravel' => 'Laravel', 'vue' => 'Vue.js', 'react' => 'React']" />
Multi-select with pills
<x-jaren::combobox label="Technologies" wire:model="stack" multiple :options="$techOptions" :max-selected="5" />
Grouped options
<x-jaren::combobox label="Assign to" wire:model="userId" grouped with-avatars with-descriptions :options="[ ['value' => 1, 'label' => 'Jane Doe', 'group' => 'Engineering', 'description' => 'Lead engineer', 'initials' => 'JD', 'color' => '#185FA5'], ['value' => 2, 'label' => 'Alex Kim', 'group' => 'Engineering', 'description' => 'Backend dev', 'initials' => 'AK', 'color' => '#7C3AED'], ['value' => 3, 'label' => 'Mia Lee', 'group' => 'Design', 'description' => 'UI/UX', 'initials' => 'ML', 'color' => '#B52676'], ]" />
Creatable (add new options on the fly)
<x-jaren::combobox label="Tags" wire:model="tags" multiple creatable :options="$existingTags" @jaren-combobox-create="handleNewTag($event.detail)" />
Async server-side search
Generate a Livewire-backed combobox:
php artisan jaren:make-combobox UserCombobox --model=User --search=name,email
Use it:
<livewire:jaren.user-combobox wire:model="userId" label="Assign to"/>
Or inline without subclassing:
<livewire:jaren.async-combobox model="\App\Models\User" label="Assign to" label-column="name" value-column="id" :searchable-columns="['name', 'email']" with-avatars wire:model="userId" />
Option shape
Every option can be a plain string (using key as value) or a full array:
[
'value' => 1, // required — submitted value
'label' => 'Jane Doe', // required — display text
'group' => 'Engineering', // optional — group header
'meta' => 'Admin', // optional — right-aligned text
'description' => 'Lead engineer', // optional — sub-label (with-descriptions)
'badge' => 'Pro', // optional — pill badge (with-badges)
'initials' => 'JD', // optional — avatar letters (with-avatars)
'color' => '#185FA5', // optional — avatar background colour
'disabled' => false, // optional — grey out and prevent selection
]
Props
| Prop | Type / Values | Default |
|---|---|---|
options |
array of strings or option arrays | [] |
multiple |
bool | false |
searchable |
bool | true |
clearable |
bool | true |
creatable |
bool — allow adding new options | false |
grouped |
bool — group by option['group'] |
false |
async |
bool — fire JS search event | false |
max-selected |
int (multiple mode) | null |
close-on-select |
bool | true (single), false (multiple) |
with-avatars |
bool | false |
with-badges |
bool | false |
with-descriptions |
bool | false |
size |
xs sm md lg xl 2xl |
md |
placeholder |
string | 'Select an option…' |
search-placeholder |
string | 'Search…' |
empty-text |
string | 'No options found' |
label |
string | — |
hint |
string | — |
error |
string | — |
Events dispatched
| Event | Payload | When |
|---|---|---|
jaren-combobox-change |
{value, option} |
Any selection change |
jaren-combobox-create |
{value, label} |
New option created |
jaren-combobox-search |
{query, callback} |
Async mode — call callback(results) |
Listen in Alpine:
<x-jaren::combobox @jaren-combobox-change="console.log($event.detail.value)" @jaren-combobox-create="$wire.addTag($event.detail.label)" ... />
Async JS search (no Livewire)
For pure client-side async (e.g. fetching from an API):
<x-jaren::combobox async label="Search users" wire:model="userId" @jaren-combobox-search=" fetch('/api/users?q=' + $event.detail.query) .then(r => r.json()) .then(data => $event.detail.callback(data)) " />
The callback receives the results array and populates the dropdown automatically.
AsyncCombobox Livewire component
Override search() in your subclass for full control:
class CountryCombobox extends \JarenUI\Livewire\AsyncCombobox { public string $label = 'Country'; public string $placeholder = 'Search countries…'; public int $minChars = 2; public int $limit = 20; public function search(string $query): array { return Country::where('name', 'like', "%{$query}%") ->orderBy('name') ->limit($this->limit) ->get() ->map(fn ($c) => [ 'value' => $c->code, 'label' => $c->name, 'meta' => $c->code, 'badge' => $c->region, ]) ->toArray(); } }
Customising views
Publish views to override any component:
php artisan jaren:publish --views
Views land in resources/views/vendor/jarenui/. Laravel will prefer these over the package defaults.
Upgrading
composer update leenuxus/jarenui php artisan jaren:publish --assets --force
Contributing
git clone https://github.com/leenuxus/jarenui
cd livewire
composer install
vendor/bin/pest
License
MIT — see LICENSE.md.