---
title: Multi-Select
description: Multi-value select with tag display
visibility: guest
draft: false
---

# 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](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) with `aria-multiselectable="true"`.

## Basic Usage

:::preview{title="Multi-Select Countries"}
<x-wirekit::multi-select
    name="countries"
    label="Countries"
    :options="[
        'de' => 'Germany',
        'fr' => 'France',
        'es' => 'Spain',
        'it' => 'Italy',
        'nl' => 'Netherlands',
        'at' => 'Austria',
        'ch' => 'Switzerland',
    ]"
    placeholder="Search countries..."
/>
:::

## With Preselected Values

:::preview{title="Preselected Tags"}
<x-wirekit::multi-select
    name="skills"
    label="Skills"
    :options="[
        'php' => 'PHP',
        'js' => 'JavaScript',
        'py' => 'Python',
        'go' => 'Go',
        'rust' => 'Rust',
    ]"
    :value="['php', 'js']"
    placeholder="Add skills..."
/>
:::

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

## Error State

:::preview{title="Multi-select with an error message"}
<x-wirekit::multi-select
    name="tags-error-demo"
    label="Tags"
    :options="['a' => 'Alpha', 'b' => 'Beta', 'c' => 'Gamma']"
    error="Please select at least one tag."
/>
:::

## With Hint

:::preview{title="Hint Text"}
<x-wirekit::multi-select
    name="roles"
    label="Roles"
    :options="['admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer']"
    hint="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:

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

See [Input — Width](/components/input#width) for more layout examples (grid columns, mixed widths).

## Options Format

The `options` prop accepts three formats — identical to the [Combobox](/components/combobox) component:

```blade
{{-- 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:

```blade
<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:

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

Load `wirekit-optimistic.js` alongside whichever bundle you already use — below it, in your layout:

```blade
@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.

:::preview{title="Optimistic multi-select — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::multi-select name="opt-accept" label="Accepted — the confirmation is silent" :value="['php']" :options="['php' => 'PHP', 'js' => 'JavaScript', 'css' => 'CSS']" optimistic="demoAccept" />
    <x-wirekit::multi-select name="opt-reject" label="Refused — the old set comes back, and the refusal is spoken" :value="['php']" :options="['php' => 'PHP', 'js' => 'JavaScript', 'css' => 'CSS']" optimistic="demoReject" />
    <x-wirekit::multi-select name="opt-slow" label="Slow answer — the dashed outline is the provisional state" :value="['php']" :options="['php' => 'PHP', 'js' => 'JavaScript', 'css' => 'CSS']" optimistic="demoSlow" />
</x-wirekit::stack>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::multi-select
    name="tags"
    label="Tags"
    :value="$tags"
    :options="['php' => 'PHP', 'js' => 'JavaScript', 'css' => 'CSS']"
    optimistic="saveTags"
/>
:::

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.

::: info 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](#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`:

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

## Further Reading

- [WAI-ARIA 1.2 Combobox Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) — the authoring pattern this component implements
- [MDN: `role="combobox"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/combobox_role)
- [MDN: `role="listbox"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role)
- [MDN: `aria-multiselectable`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-multiselectable)
- [MDN: `aria-selected`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-selected)
