Skip to main content
Copy for LLM

Select

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

Basic Usage

A labeled select

Where the invoice is billed.

With Placeholder

With a placeholder

With Error

Error state

Please select a role.

Errors from Laravel's validation bag are shown automatically when name matches a field, also for a nested name such as items[0][qty], whose error Laravel stores as items.0.qty, and for a list such as tags[].

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.

Success / valid state

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, or 'lang' => 'de' to mark which language that entry's text is in — see Language pickers.

Grouped + disabled options
<x-wirekit::select label="Destination" name="destination" :options="[
    // A nested array under a label key becomes an optgroup with that label
    '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:

Options from a slot

Nothing to Choose

A select filled from your records has no option until the first record exists, and a placeholder alone tells the reader neither what is missing nor where to add it. empty-text says what is missing, and empty-href links to the screen that creates it. While the select has no option, it is disabled and the sentence below it is its description, so a screen reader on the field hears why it is empty:

A select with nothing to choose
There are no suppliers yet. Add a supplier

The three props only take effect while there is nothing to choose. As soon as options holds an entry, or an <option> arrives through the slot, the select renders as it always does. Pass the records straight in and let the select decide:

<x-wirekit::select
    label="Supplier"
    name="supplier_id"
    wire:model="supplierId"
    placeholder="Choose a supplier"
    :options="$suppliers->pluck('name', 'id')"
    empty-text="There are no suppliers yet."
    :empty-href="auth()->user()->can('create', Supplier::class) ? route('suppliers.create') : null"
    empty-action="Add a supplier"
/>

Name what the link creates in empty-action. Without it the link reads "Create one". Leave empty-href null for a reader who may not create the record: the sentence still explains the empty field, and no link leads to a page that would refuse them.

Bound to a property declared int, float or an enum backed by int, such as the id of a record or a priority, the select is read as a number: wire:model goes out as wire:model.number. A select gives its value as text, and Livewire would take the server's typed answer for a change made on the server and set the select back to it while a newer choice is on its way. A property without a type, or a string one, gets the text.

In an application that moves between its screens with wire:navigate, add empty-navigate and the link does the same instead of reloading the page. A link to a fragment of the same page keeps the browser's own jump.

For a control of your own, such as a button that opens a dialog to add the record in place, the empty slot replaces the sentence and the link. In your application options comes from your records, as above:

Your own control in place of the link
There are no locations yet.

Width

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

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

See 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> (single value, client-side filtering) or <x-wirekit::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:

<x-wirekit::select
    label="Plan"
    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:

@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></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 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.

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. That page is also where the boundary is stated: this covers mutations, not sorting, filtering or pagination.

Language pickers

A language picker lists endonyms — Deutsch, Español, Français — so most of its labels are words in a language the page is not written in. Without a marker a screen reader pronounces all of them in the page's voice, and this is the one control that exists for the people who would notice.

Give each entry a lang:

{{-- 1. The key is the value submitted; 'label' is what the reader sees. --}}
{{-- 2. 'lang' is the BCP 47 tag for the label's own language, not for the page. --}}
<x-wirekit::select
    label="Language"
    name="locale"
    :options="[
        'de' => ['label' => 'Deutsch', 'lang' => 'de'],
        'es' => ['label' => 'Español', 'lang' => 'es'],
        'fr' => ['label' => 'Français', 'lang' => 'fr'],
    ]"
/>

This is WCAG 2.1 AA 3.1.2 (Language of Parts). The proper-name exception does not cover it: an endonym is the language's own word for itself, not a borrowed one.

Leave flags out of a language picker. A flag names a country, and a language is rarely one country's: Spanish is spoken as a first language across many countries, Portuguese belongs to Brazil as much as to Portugal, and English sits under several flags at once. Any single flag misnames the language and tells some of its speakers they are guests, while the endonym says exactly what it means.

An automated scan will not tell you this is missing

axe's valid-lang rule carries the wcag312 tag, but it validates a lang attribute that is present. With none in the document there are no nodes to judge and the scan reports zero violations — so a picker with no lang anywhere passes an audit and silently mispronounces every entry. Do not read a green accessibility run as an answer to this question; you must check the markup.

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.
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
help string|null null An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label
reserveMessage bool false Keep the message line's height even when there is no message — see 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-compact, md, lg. md-compact is the 2.25rem tier between sm and md, for dense toolbars — see Input for the worked example
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, 'lang' => 'de']]). Mix freely.
emptyText string|null null What the field says while it has no option: no entry in options and none in the slot. The select is then disabled and the sentence is its description. See Nothing to Choose
emptyHref string|null null Where the missing record is created. Renders a link after emptyText
emptyAction string|null null The link's text, naming what it creates. Without it the link reads "Create one"
emptyNavigate bool false Draw the link with wire:navigate, so it changes screens without reloading the page. Never on a link to a fragment of the same page
scope string|null null Scoped personalization name
announceError bool|null null Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region.

Slots

Slot Description
Default <option> elements of your own, after the ones built from options.
empty A control of your own in place of the sentence and the link shown while there is nothing to choose. See Nothing to Choose.
labelAfter Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label.

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
  • With nothing to choose, the select is disabled and names the empty-text line in aria-describedby. The link sits outside the select, so it stays in the tab order
  • 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 a plain list to :options. The array KEY is what the option submits, so ['Germany', 'Austria'] renders value="0" / value="1" — the labels read correctly and the form submits array indexes. A list of ['value' => …, 'label' => …] entries does the same thing for the same reason: its keys are still 0 and 1. Pass an associative array (['de' => 'Germany']), a grouped array, or per-option attrs — see Option Groups & Disabled Options.
  • 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-compact / --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:

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

See Also

  • Inline Edit — edit this value in place, with an explicit confirm step

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store