---
title: Select
description: Native select dropdown
visibility: guest
draft: false
---

# Select

A dropdown select component with label, error handling, and placeholder support.

## Basic Usage

:::preview{title="A labeled select"}
<x-wirekit::select
    label="Country"
    name="country"
    :options="['de' => 'Germany', 'at' => 'Austria', 'ch' => 'Switzerland']"
    wire:model="country"
/>
:::

## With Placeholder

:::preview{title="With a placeholder"}
<x-wirekit::select
    label="Role"
    name="role"
    placeholder="Choose a role..."
    :options="['admin' => 'Admin', 'editor' => 'Editor']"
/>
:::

## With Error

:::preview{title="Error state"}
<x-wirekit::select
    label="Role"
    name="role"
    :options="['admin' => 'Admin', 'editor' => 'Editor']"
    error="Please select a role."
/>
:::

Errors from Laravel's validation bag are shown automatically when `name` matches a field.

## With Success

Confirm a valid selection with a green border and optional message — the mirror of the error state, but valid (no `aria-invalid`). `error` wins when both are set.

:::preview{title="Success / valid state"}
<x-wirekit::select
    label="Plan"
    name="plan"
    :options="['pro' => 'Pro']"
    success="Plan confirmed"
/>
:::

## Option Groups & Disabled Options

The `:options` array accepts three shapes you can mix freely. Nest an array under a label key to render an `<optgroup>`; give an option an array value with `'disabled' => true` to disable it.

:::preview{title="Grouped + disabled options"}
<x-wirekit::select label="Destination" name="destination" placeholder="Choose a city…" :options="[
    'Europe' => ['ber' => 'Berlin', 'par' => 'Paris'],
    'Asia' => ['tyo' => 'Tokyo', 'sin' => ['label' => 'Singapore (sold out)', 'disabled' => true]],
]" />
:::

```blade
<x-wirekit::select name="destination" :options="[
    // Nested array under a label key → <optgroup label="Europe">
    'Europe' => ['ber' => 'Berlin', 'par' => 'Paris'],
    // An option whose value is ['label' => ..., 'disabled' => true] is disabled
    'Asia' => ['tyo' => 'Tokyo', 'sin' => ['label' => 'Singapore (sold out)', 'disabled' => true]],
]" />
```

## Slot Options

For more control, use the default slot instead of the `:options` prop:

:::preview{title="Options from a slot"}
<x-wirekit::select label="Priority" name="priority">
    <option value="">Select priority...</option>
    <option value="low">Low</option>
    <option value="medium">Medium</option>
    <option value="high">High</option>
</x-wirekit::select>
:::

## Width

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

```blade
<div class="max-w-xs">
    <x-wirekit::select label="Country" name="country" :options="$countries" />
</div>
```

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

## On Mobile

Because this component is a real `<select>`, the **open dropdown is drawn by the operating system**, not by WireKit — so on a phone or tablet it appears as the platform's native picker (the iOS wheel sheet, the Android dropdown). That is deliberate: the native picker is the most accessible, most familiar, and most touch-friendly option list a mobile user can get, and it needs no extra JavaScript.

One thing to know when testing: a desktop browser's **device-emulation mode** (e.g. DevTools' responsive view) draws that OS popup against the real window rather than the emulated viewport, so the option list can look small or mis-placed there. That is an artifact of the emulator — on a real device the picker opens correctly. Test native pickers on an actual phone, or trust the emulator only for the trigger, not the OS popup.

If you instead need a dropdown that renders **consistently across every device and is fully themeable** — with search, custom option markup, or multi-value selection — reach for [`<x-wirekit::combobox>`](/components/combobox) (single value, client-side filtering) or [`<x-wirekit::multi-select>`](/components/multi-select) (multiple values). Both are custom listboxes built on Alpine + Floating UI, so their open panel is WireKit-styled and positions identically everywhere.

## Optimistic UI

Pass the name of the Livewire method this component should call and the change appears immediately, then confirms or undoes itself when the server answers:

```blade
<x-wirekit::select
    name="plan"
    :value="$plan"
    optimistic="choosePlan"
>
    <option value="free">Free</option>
    <option value="pro">Pro</option>
</x-wirekit::select>
```

Load `wirekit-optimistic.js` alongside whichever bundle you already use — it is a separate file so applications that do not use it pay nothing for it:

```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 select — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::select name="opt-accept" label="Accepted — the confirmation is silent" value="pro" :options="['free' => 'Free', 'pro' => 'Pro', 'team' => 'Team']" optimistic="demoAccept" />
    <x-wirekit::select name="opt-reject" label="Refused — the old choice comes back, and the refusal is spoken" value="pro" :options="['free' => 'Free', 'pro' => 'Pro', 'team' => 'Team']" optimistic="demoReject" />
    <x-wirekit::select name="opt-slow" label="Slow answer — the dashed outline is the provisional state" value="pro" :options="['free' => 'Free', 'pro' => 'Pro', 'team' => 'Team']" 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::select
    name="plan"
    :value="$plan"
    optimistic="choosePlan"
>
    <option value="free">Free</option>
    <option value="pro">Pro</option>
</x-wirekit::select>
:::

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.

A second change while one is in flight is **refused** rather than queued, and that matters more here than on a toggle: queued, the final selection would be whichever answer arrived last, which is network timing.

**What a screen reader hears** is the same for every component that supports this, and it is written out in full — one hedged announcement at the change, silence on confirmation, one more only if the server refuses, and focus that never moves — in [Optimistic UI — the announcement contract](/extending/optimistic-ui). That page is also where the boundary is stated: this covers mutations, not sorting, filtering or pagination.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `optimistic` | `string\|null` | `null` | Livewire method to call, showing the new value before the server confirms it. 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. |
| `label` | string\|null | `null` | Label text above the select |
| `hideLabel` | bool | `false` | Render the label visually hidden (`sr-only`) but keep it for assistive tech — for a compact field in a toolbar / header |
| `hint` | string\|null | `null` | Help text below the select |
| `reserveMessage` | bool | `false` | Keep the message line's height even when there is no message — see [Fields in a row](/components/input#fields-in-a-row) |
| `error` | string\|null | `null` | Error message (overrides `$errors` bag) |
| `success` | string\|bool\|null | `null` | Valid-state confirmation — string shows a green message, `true` shows just the green border. `error` wins. |
| `size` | string | `'md'` | `sm`, `md`, `lg` |
| `placeholder` | string\|null | `null` | Placeholder option text |
| `value` | string\|null | `null` | Pre-selected option key. Compared as a string, so `1` and `'1'` both match |
| `options` | array | `[]` | Options. Flat (`['de' => 'Germany']`), grouped (`['Europe' => ['de' => 'Germany']]` → `<optgroup>`), or per-option attrs (`['de' => ['label' => 'Germany', 'disabled' => true]]`). Mix freely. |
| `scope` | string\|null | `null` | Scoped personalization name |

## Accessibility

- Built on the native `<select>` element — full browser keyboard support out of the box
- Label is a proper `<label for="...">` matched to the select's `id`
- Error messages linked via `aria-describedby` and `aria-invalid="true"`
- Hint text linked via `aria-describedby`
- Native `<select>` supports arrow keys, Home/End, type-to-search, and screen reader announcements by default

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Tab` | Move focus to the select |
| `Space` / `Enter` / `ArrowDown` | Open the option list |
| `ArrowUp` / `ArrowDown` | Move between options |
| `Home` / `End` | Jump to first / last option |
| Type a character | Type-ahead — focus next option whose label starts with that character |
| `Escape` | Close the option list without changing the value |

## Pitfalls

- **Don't pass an array of strings to `:options`.** The component expects `[{ value, label }]` shape. Plain strings render as `<option>{string}</option>` with no value, breaking form submit.
- **Don't use `wire:model.live` for huge option lists.** Each keystroke during type-ahead would round-trip to the server. Use `wire:model.blur` or convert to `<x-wirekit::combobox>` with client-side filtering.

## Design Tokens

| Token | Used for |
| --- | --- |
| `--font-wk-sans` | Body font family |
| `--font-wk-letter-spacing` | Letter spacing |
| `--text-wk-sm` / `--text-wk-md` / `--text-wk-lg` | Font size per `size` prop |
| `--color-wk-text` | Selected option text |
| `--color-wk-text-muted` / `--color-wk-text-subtle` | Placeholder + chevron |
| `--color-wk-bg-input` | Select background |
| `--color-wk-border-strong` | Default border |
| `--color-wk-border-strong-hover` | Hover border |
| `--color-wk-border-error` | Error-state border |
| `--color-wk-danger` / `--color-wk-danger-text` | Error message |
| `--color-wk-ring` / `--color-wk-ring-offset` | Focus ring color + offset |
| `--ring-wk-width` / `--ring-wk-offset` | Focus ring geometry |
| `--border-wk-width` | Border width |
| `--radius-wk-sm` / `--radius-wk-md` | Border radius |
| `--shadow-wk-sm` | Subtle shadow |
| `--size-wk-sm` / `--size-wk-md` / `--size-wk-lg` | Control height per `size` prop |
| `--padding-wk-x-sm` / `--padding-wk-x-md` / `--padding-wk-x-lg` | Horizontal padding |
| `--opacity-wk-disabled` | Disabled visual weight |
| `--transition-wk-duration` / `--transition-wk-easing` | Hover / focus transition |

## Config Defaults

The defaults live in `config/wirekit.php` under `components.select`. Override them globally:

```php
'components' => [
    'select' => ['size' => 'lg'], // change default size
],
```

## See Also

- [Inline Edit](/components/inline-edit) — edit this value in place, with an explicit confirm step

## Further Reading

- [MDN: `<select>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select)
- [MDN: `<option>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option)
- [MDN: Styling `<select>`](https://developer.mozilla.org/en-US/docs/Learn/Forms/Advanced_form_styling#styling_select_boxes)
- [WebAIM: Creating Accessible Forms](https://webaim.org/techniques/forms/) — form accessibility primer
