leenuxus/jarenui

A Jaren-UI compatible Livewire component library for Laravel — 50+ components, dark mode, CSS-variable theming.
13
Install
composer require leenuxus/jarenui
Latest Version:v1.4.0
PHP:^8.4
License:MIT
Last Updated:Jun 3, 2026
Links: GitHub  ·  Packagist
Maintainer: leenuxus

JarenUI for Laravel Livewire

Latest Version PHP Version License

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 @jarenStyles into 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: toggleable occupies the trailing addon slot. Do not combine it with clearable, copyable, icon-end, or suffix on 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 06 (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 06 (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 class
  • resources/views/livewire/onboarding-wizard/account.blade.php
  • resources/views/livewire/onboarding-wizard/plan.blade.php
  • resources/views/livewire/onboarding-wizard/features.blade.php
  • resources/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.