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.

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 unlabelled.
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"
  • Selected pills: each has a remove button with aria-label="Remove {label}"
  • Empty state: "No results" message when filter produces no matches
  • Error state: aria-invalid="true" + aria-describedby linking to error message
  • Selection changes announced via aria-live="polite" region

Keyboard Interaction

Key Action
Tab Move focus to the multi-select trigger
Enter / Space / ArrowDown Open the listbox
ArrowUp / ArrowDown Move the active option
Enter / Space Toggle the active option's selection
Home / End Jump to first / last option
Type to search Filter options (case-insensitive substring match)
Backspace (in search field, empty input) Remove the last selected chip
Escape Close the listbox

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?

Thanks — that helps.

Voting requires cookies or local storage. What we store