Skip to main content
WireKit
Copy for LLM

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

Country Picker

With Preselected Value

Combobox with Initial Selection

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:

Combobox with disabled premium 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:

Combobox with grouped options

Size Variants

Three Sizes

Width

Like all WireKit form components, the combobox fills its container (w-full). Control the width via the parent element:

Combobox in a 24rem container
<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.

Optimistic combobox — accepted, refused, and a slow answer

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 label for a visible label (associated with the input via for), hideLabel to keep that label for screen readers while hiding it visually, or ariaLabel to 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 via aria-controls
  • Each option: role="option", aria-selected
  • Empty state: "No results" message when filter produces no matches
  • Error state: aria-invalid="true" + aria-describedby linking to error message
Labeled combobox

For a compact toolbar where a visible label would crowd the layout, keep the name for assistive tech with ariaLabel:

Toolbar combobox with an aria-label

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.live on the search input. Every keystroke round-trips. Use the component's built-in client-side filtering and bind wire:model to 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

Was this page helpful?

Thanks — that helps.

Voting requires cookies or local storage. What we store