Segmented Control
The <x-wirekit:: component is a pill-style radio group for switching between a small set of mutually exclusive options. It uses role="radiogroup" with role="radio" on each segment, providing full keyboard navigation via arrow keys.
Basic Usage
Without Preselected Value
Size Variants
Narrow Screens
When the options are wider than the space the control sits in, the bar scrolls on its own instead of widening the page, the way tabs do, and the selected option is scrolled into view when the page loads. A soft shadow at an edge says more options sit past it, and it moves to the other edge as the bar scrolls. Each option keeps its label on one line. The preview below fits on a wide screen; at phone width the bar scrolls.
Translations lengthen labels, so a bar that fits in one language can overflow in the next. Keep labels short all the same: a set of options a phone cannot show at once is easier to use as tabs or a select.
Options in Another Language
An option can be an array with its label and the lang its words are in, the shape select takes. A choice of languages names each one in its own language, and the lang makes a screen reader say it in that language's voice instead of the page's. Plain value => label options and arrays mix freely.
Livewire Integration
<x-wirekit::segmented-control
label="Sort"
name="sort"
:options="['newest' => 'Newest', 'oldest' => 'Oldest', 'popular' => 'Popular']"
:value="$sortOrder"
wire:model.live="sortOrder"
/>
The selected value is submitted via a hidden <input type="hidden"> — wire:model binds to it automatically. Pass the bound property as :value as well: the control keeps the selected segment in its own state, seeded from value, so without it a stored choice is not shown and a value the server sets does not move the selection.
Bound to a property declared int, float or an enum backed by int, the hidden input is read as a number: wire:model.live goes out as wire:model.. It holds the segment as text, and Livewire would take the server's typed answer to that text for a change made on the server and write it over a segment chosen while the request was out. A property without a type, or a string one, gets the text.
Optimistic UI
Pass the name of the Livewire method the control should call and the segment moves immediately, then confirms or undoes itself when the server answers:
<x-wirekit::segmented-control
name="tier"
label="Tier"
:value="$tier"
:options="['basic' => 'Basic', 'plus' => 'Plus', 'max' => 'Max']"
optimistic="chooseTier"
/>
Load wirekit-optimistic. alongside whichever bundle you already use — below it, in your layout:
@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.
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.
The prop takes a method name rather than true because the component cannot know the action otherwise: server actions reach a WireKit component through the attribute bag, so it never sees your wire:click. optimistic replaces wire:model.live here rather than joining it — the method you name is what changes the value on the server; pass the current one with :value so the control knows where it started.
Arrow keys go through the same path as a click: selection follows focus in a radio group, so moving with the keyboard is a pick like any other and behaves identically.
Picking a segment announces once, hedged — "Saving" — so the new value 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.
Table view is unavailable while the export is running.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string|null |
null |
Accessible group label |
error |
string|null |
null |
Validation message. Rendered below the control, announced politely, and wired with aria-invalid + aria-describedby |
hint |
string|null |
null |
Helper text below the control. An error replaces it — one message region, so the two cannot stack |
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 |
name |
string|null |
null |
Form field name |
id |
string|null |
auto-generated | Base element id, on the radio group. A <label for> cannot name a group: use label, aria-label or aria-labelledby |
options |
array |
[] |
value => label pairs; a label can be an array with label and lang, see Options in Another Language |
value |
string|null |
null |
Initially selected value |
optimistic |
string|null |
null |
Livewire method to call, moving the segment before the server confirms it. Ignored when disabled. See Optimistic UI. |
optimisticArgs |
array |
[] |
Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
size |
string |
'md' |
'sm', 'md', 'lg' |
disabled |
bool |
false |
Disables every segment, which stays reachable by keyboard and announced as unavailable. Like a disabled native field, it is left out of the data a form submits |
scope |
string|null |
null |
Scoped personalization key |
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 |
|---|---|
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
- Outer container:
role="radiogroup"witharia-labelfrom thelabelprop - Each segment:
role="radio"witharia-checked="true"/"false" - An
autofocuswritten on the tag goes to the selected segment, which holds the tab stop - Selection change announced automatically by screen readers via the radio role semantics
- The selected segment is a raised surface in
--font-wk-selected-weight. The surface marks it on its own, so a theme may set that weight to the body weight. Under forced colors, where the surface and its shadow are not painted, the selected segment is framed in the system's selection color - Disabled state sets
aria-disabled="true"on the radiogroup and each segment - Focus ring is visible on the focused segment via
focus-visiblestyling - Every segment keeps the 24px minimum target of WCAG 2.5.8 in all three sizes, whatever size your theme gives the text —
--size-wk-target-minsets the floor
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Move focus into the control |
| ArrowRight / ArrowDown | Move focus and selection to the next segment, wrapping to the first |
| ArrowLeft / ArrowUp | Move focus and selection to the previous segment, wrapping to the last |
| Home | Select the first segment |
| End | Select the last segment |
Selection follows focus, and both arrow axes wrap at the ends — the same behavior a native radio group has.
The component uses roving tabindex — only the active segment has tabindex="0".
Pitfalls
- Keep the options to a handful. On a narrow screen the bar scrolls instead of widening the page (see Narrow Screens), but an option a reader has to scroll to find is easier to reach in
<x-wirekit::ortabs> <x-wirekit::.select>
Design Tokens
| Token | Purpose |
|---|---|
--color-wk-bg-muted |
Track background |
--color-wk-bg-elevated |
Active segment pill background |
--color-wk-text |
Active segment text color |
--color-wk-text-muted |
Inactive segment text color |
--color-wk-ring |
Focus ring color |
--shadow-wk-sm |
Active segment pill shadow |
--font-wk-selected-weight |
Active segment weight |
--size-wk-target-min |
The floor of a segment's height; padding and text size set the rest |
--radius-wk-md |
Track border radius |
--radius-wk-sm |
Segment pill border radius |
--padding-wk-x-sm / md / lg |
Horizontal padding per size variant |
--text-wk-sm / md / lg |
Font size per size variant |
--transition-wk-duration |
Transition of the selected segment's surface and color |
--opacity-wk-disabled |
Dimmed state when disabled |
Customization
Override defaults in config/:
'components' => [
'segmented-control' => ['size' => 'md'],
],