Skip to main content
WireKit
Copy for LLM

Multi-Select

The <x-wirekit::multi-select> component is a combobox that supports selecting multiple values. Selected items appear as removable pills inside the input area, and the dropdown filters options as the user types. It implements the WAI-ARIA 1.2 Combobox pattern with aria-multiselectable="true".

Basic Usage

Multi-Select Countries

With Preselected Values

Preselected Tags

The value prop accepts an array of option keys that should be pre-selected on load.

Error State

Multi-select with an error message

Please select at least one tag.

With Hint

Hint Text

Assign one or more roles to this user.

Width

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

<div class="max-w-md">
    <x-wirekit::multi-select label="Tags" name="tags" :options="$tags" />
</div>

See Input — Width for more layout examples (grid columns, mixed widths).

Options Format

The options prop accepts three formats — identical to the Combobox component:

{{-- 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'],
]"

Form Submission

Each selected value is submitted via a hidden <input type="hidden" name="name[]">. This means Laravel receives a standard array — no special parsing needed on the backend.

Livewire Binding

Bind the array of selected values with wire:model on the component tag:

<x-wirekit::multi-select wire:model.live="tags" name="tags" :options="$options" :value="$tags" />

The selection lives in Alpine and is exposed to the binding, so the property receives the full array on every pick and every removal. Pass the current value with :value as well — that seeds the initial selection, the same framework-agnostic pattern the other stateful controls use.

Optimistic UI

Pass the name of the Livewire method the control should call and the pill appears — or disappears — immediately, then confirms or undoes itself when the server answers:

<x-wirekit::multi-select
    name="tags"
    label="Tags"
    :value="$tags"
    :options="['php' => 'PHP', 'js' => 'JavaScript', 'css' => 'CSS']"
    optimistic="saveTags"
/>

The method receives the full new selection as an array, not the value that changed:

public function saveTags(array $tags): void
{
    $this->tags = $tags;
}

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 multi-select — 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.

Focus stays on the option you clicked rather than returning to the filter, so you can keep picking. Removing a pill takes the same path as picking an option: it is the same server mutation, so it is undone the same way. The filter text is separate state and is never rolled back — an undo puts back the server's selection, never what you were typing.

What a screen reader hears Picking or removing announces once, hedged — "Saving" — so the change 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 control 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
label string|null null Label text above the input
hint string|null null Help text below the input
error string|null null Error message (also reads from $errors)
name string|null null Form field name (submits as name[])
id string|null auto-generated Element id
options array [] Options (associative, list, or array-of-arrays)
value array [] Initially selected values
optimistic string|null null Livewire method to call, showing the new selection before the server confirms it. Receives the full selection as an array. See Optimistic UI.
optimisticArgs array [] Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to.
placeholder string 'Select...' Placeholder text in the filter input
disabled bool false Disabled state
ariaLabel string|null null Explicit aria-label for the internal combobox <input>. When unset, the component auto-derives a label from (in order) any passthrough aria-label attribute, then label, then placeholder, then name — so the internal input is never unlabeled.
scope string|null null Scoped personalization key

Accessibility

  • Filter input: role="combobox", aria-expanded, aria-controls, aria-autocomplete="list", aria-multiselectable="true"
  • Listbox: role="listbox" with aria-multiselectable="true" and unique id linked via aria-controls
  • Each option: role="option", aria-selected="true" / "false", and its own id
  • Keyboard position: aria-activedescendant on the filter input names the active option. Focus stays in the input for the whole interaction — that is what makes a combobox a combobox — so this attribute is what tells a screen reader which option Enter would take
  • Selected pills: each has a remove button with aria-label="Remove {label}"
  • Empty state: "No results" message when the filter matches nothing
  • Error state: aria-invalid="true" + aria-describedby linking to error message
  • Selection changes announced via an aria-live="polite" region. With optimistic set, that region is not rendered and the optimistic layer announces the pick instead — one voice per change, so that hearing a second announcement always means something went differently

Keyboard Interaction

Key Action
Tab Move focus to the filter input, which opens the listbox
Enter / ArrowDown / ArrowUp Open the listbox after Escape closed it. ArrowDown opens on the first option, ArrowUp on the last
ArrowDown / ArrowUp Move the active option. Stops at either end rather than wrapping
Enter Toggle the active option's selection
Home / End Jump to first / last option
Type to search Filter options (case-insensitive substring match), and restart the active option at the top
Backspace (in search field, empty input) Remove the last selected chip
Escape Close the listbox

Space types a space. It is not bound to anything here, and deliberately so: this is an editable combobox rather than a select-only one, and a text field that swallows the space bar cannot be typed into. Use Enter to toggle.

Pitfalls

  • Don't use multi-select for binary on/off groups. A row of <x-wirekit::checkbox> is more discoverable. Multi-select is for moderate-to-large option lists where space is at a premium.
  • Don't set :options to a list of plain strings. The component expects [{ value, label }] shape — strings render as objects with undefined value and break form submit.

Design Tokens

Token Purpose
--color-wk-bg-input Input area background
--color-wk-bg-muted Selected pill background
--color-wk-bg-elevated Dropdown panel background
--color-wk-border-strong Default border color
--color-wk-border-error Border color on error
--color-wk-text Text color
--color-wk-text-muted Placeholder and pill remove icon color
--color-wk-accent Highlighted option background
--color-wk-accent-fg Highlighted option text color
--color-wk-ring Focus ring color
--radius-wk-md Border radius for input and dropdown
--radius-wk-sm Border radius for pills
--shadow-wk-md Dropdown panel shadow
--transition-wk-duration Open/close and highlight transitions

Customization

Override defaults in config/wirekit.php:

'components' => [
    'multi-select' => ['placeholder' => 'Choose...'],
],

Further Reading

Was this page helpful?

Voting requires cookies or local storage. What we store