Combobox
The <x-wirekit::combobox> component is a searchable select that filters options as the user types. It implements the WAI-ARIA 1.2 Combobox pattern with full keyboard navigation.
Usage
With Preselected Value
The value prop matches an option's value key — the label is looked up and pre-filled into the search input on load.
Disabled Options
Pass disabled => true on an option to render it dimmed and non-interactive — keyboard arrow-navigation skips it, mouse hover doesn't highlight it, click + Enter ignore it. Useful for premium-tier features, "coming soon" entries, or context-locked options:
The aria-disabled="true" attribute on disabled options keeps screen readers correctly informed.
Grouped Options
Organize options under headings by passing a nested array — a group is an array value keyed by its group label, mirroring <x-wirekit::select>. Each group renders as a role="group" with an accessible label; filtering hides empty groups automatically, and arrow-key navigation flows across the group boundaries as one continuous list:
Size Variants
Width
Like all WireKit form components, the combobox fills its container (w-full). Control the width via the parent element:
<div class="max-w-sm">
<x-wirekit::combobox label="Country" name="country" :options="$countries" />
</div>
See Input — Width for more layout examples (grid columns, mixed widths).
Options Format
The options prop accepts four formats:
{{-- Associative: key => label --}}
:options="['de' => 'Germany', 'fr' => 'France']"
{{-- List of strings (value === label) --}}
:options="['Apple', 'Banana', 'Cherry']"
{{-- List of arrays --}}
:options="[
['value' => 'de', 'label' => 'Germany'],
['value' => 'fr', 'label' => 'France'],
]"
{{-- Grouped: group label => sub-options (any of the shapes above) --}}
:options="[
'Europe' => ['de' => 'Germany', 'fr' => 'France'],
'Asia' => ['jp' => 'Japan'],
]"
Form Submission
The selected value (not label) is submitted via a hidden <input type="hidden"> with the given name. The visible text input is for search + display only.
Livewire Integration
<x-wirekit::combobox> keeps its selected value in internal state seeded from
the value prop, so when binding with Livewire pass the bound property as
:value alongside wire:model to seed the initial selection:
<x-wirekit::combobox wire:model.live="country" :value="$country" :options="$countries" />
Without :value the combobox shows its placeholder until the user picks an
option; wire:model keeps it in sync afterward. This is the framework-agnostic
seeding pattern WireKit's stateful controls share — it works in plain Blade
forms too.
Optimistic UI
Pass the name of the Livewire method the combobox should call and the choice lands immediately, then confirms or undoes itself when the server answers:
<x-wirekit::combobox
name="country"
label="Country"
:value="$country"
:options="['de' => 'Germany', 'fr' => 'France', 'es' => 'Spain']"
optimistic="saveCountry"
/>
Load wirekit-optimistic.js alongside whichever bundle you already use — below it, in your layout:
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></script>
Try it
The demo below runs the real path: the change shows immediately, the outline says it is provisional, and the server's answer either confirms it silently or takes it back.
The <livewire:demos.…> wrapper above exists only on this site — it supplies the demo
methods so the page can show a real round trip. The block under it is what you write.
The prop takes a method name rather than true because the component cannot know the action otherwise: server actions reach a WireKit component through the attribute bag, so it never sees your wire:click. optimistic replaces wire:model.live here rather than joining it — the method you name is what changes the value on the server; pass the current one with :value so the combobox knows where it started.
All three ways of choosing take the same path: clicking an option, pressing Enter on the highlighted one, and the clear button. Clearing calls your method with null — choosing nothing is still a choice, and the server has to hear about it. Pressing Enter with nothing highlighted sends nothing at all.
After an undo the text field shows the label of the previous selection, not the one that was refused. The field follows the value rather than the click, which is what makes that possible.
What a screen reader hears Choosing announces once, hedged — "Saving" — so the new value is audible as provisional. Confirmation is silent: what was announced is what happened. Only a deviation speaks a second time, which is what makes an undo recognizable as an undo.
Where the combobox sits in a form that already shows a validation message, the undo stays silent and leaves that message to speak: it tells you what to do, "could not save" does not.
An aborted request announces nothing at all — nothing was refused.
Focus stays exactly where you put it. An undo arrives on the server's schedule, and moving focus then would take you out of your place for a reason you could not predict.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string|null |
null |
Form field name (submits the selected value) |
id |
string|null |
auto-generated | Element id |
options |
array |
[] |
Options (see Options Format below) |
value |
string|null |
null |
Initial selected value |
optimistic |
string|null |
null |
Livewire method to call, showing the new choice before the server confirms it. Ignored when disabled. See Optimistic UI. |
optimisticArgs |
array |
[] |
Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
size |
string |
'md' |
'sm', 'md', 'lg' |
placeholder |
string |
'Select...' |
Placeholder text |
label |
string|null |
null |
Visible label, associated with the input via for |
hideLabel |
bool |
false |
Keep the label in the DOM for assistive tech but hide it visually (compact toolbar / header fields) |
ariaLabel |
string|null |
null |
Accessible name applied directly to the input when there is no visible label |
disabled |
bool |
false |
Disabled state |
error |
string|null |
null |
Error message (also reads from $errors) |
announceError |
bool |
true |
Announce the error in an aria-live="polite" region (defaults from a11y.announce_error). |
scope |
string|null |
null |
Scoped personalization key |
Accessibility
- Accessible name (required): give every combobox a name. Use
labelfor a visible label (associated with the input viafor),hideLabelto keep that label for screen readers while hiding it visually, orariaLabelto name the input directly when the surrounding UI already makes its purpose clear (e.g. a compact facet toolbar). Without one, the control fails WCAG 4.1.2. - Text input:
role="combobox",aria-expanded,aria-controls,aria-autocomplete="list" - Listbox:
role="listbox"with unique id linked viaaria-controls - Each option:
role="option",aria-selected - Empty state: "No results" message when filter produces no matches
- Error state:
aria-invalid="true"+aria-describedbylinking to error message
For a compact toolbar where a visible label would crowd the layout, keep the
name for assistive tech with ariaLabel:
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus into the combobox input |
ArrowDown / ArrowUp |
Open the listbox and move the active option |
Enter |
Select the active option and close |
| Type a character | Filter options (case-insensitive substring match) |
Home / End |
Move the active option to first / last |
Escape |
Close the listbox without changing the value |
Pitfalls
- Don't use combobox for a known-small option list (≤6 items). A native
<x-wirekit::select>is faster, simpler, and screen-reader-friendlier. Comboboxes shine when filtering matters. - Don't bind
wire:model.liveon the search input. Every keystroke round-trips. Use the component's built-in client-side filtering and bindwire:modelto the selected value only.
Design Tokens
| Token | Used for |
|---|---|
--text-wk-xs / --text-wk-sm / --text-wk-md / --text-wk-lg |
Font size per size prop |
--color-wk-text |
Selected option text |
--color-wk-text-muted |
Empty-state + chevron text |
--color-wk-text-placeholder |
Placeholder text |
--color-wk-bg-input |
Trigger background |
--color-wk-bg-elevated |
Listbox panel background |
--color-wk-bg-muted / --color-wk-bg-subtle |
Option hover + active background |
--color-wk-accent |
Active highlight + selected check |
--color-wk-border-strong |
Default border |
--color-wk-border-error |
Error-state border |
--color-wk-danger-text |
Error message |
--color-wk-ring |
Focus ring |
--ring-wk-width |
Focus ring width |
--border-wk-width |
Border width |
--radius-wk-sm / --radius-wk-md |
Border radius |
--shadow-wk-md |
Listbox panel shadow |
--size-wk-sm / --size-wk-md / --size-wk-lg |
Trigger height per size prop |
--padding-wk-x-md / --padding-wk-y-xs / --padding-wk-y-sm |
Trigger + option padding |
--opacity-wk-disabled |
Disabled visual weight |
--transition-wk-duration |
Hover / focus transition |
--z-wk-dropdown |
Listbox stacking context |
Config Defaults
The defaults live in config/wirekit.php under components.combobox. Override them globally:
'components' => [
'combobox' => ['size' => 'md', 'placeholder' => 'Type to search...'],
],
Further Reading
- WAI-ARIA 1.2 Combobox Pattern — the authoring pattern this component implements
- MDN:
role="combobox" - MDN:
role="listbox" - MDN:
aria-autocomplete - MDN:
aria-expanded