Segmented Control
The <x-wirekit::segmented-control> 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
Livewire Integration
<x-wirekit::segmented-control
label="Sort"
name="sort"
:options="['newest' => 'Newest', 'oldest' => 'Oldest', 'popular' => 'Popular']"
wire:model.live="sortOrder"
/>
The selected value is submitted via a hidden <input type="hidden"> — wire:model binds to it automatically.
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.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.
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.
What a screen reader hears 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.
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 |
name |
string|null |
null |
Form field name |
id |
string|null |
auto-generated | Base element id |
options |
array |
[] |
Associative array of value => label pairs |
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 |
Disabled state |
scope |
string|null |
null |
Scoped personalization key |
Accessibility
- Outer container:
role="radiogroup"witharia-labelfrom thelabelprop - Each segment:
role="radio"witharia-checked="true"/"false" - Selection change announced automatically by screen readers via the radio role semantics
- The sliding pill indicator is
aria-hidden="true"— purely decorative - Disabled state sets
aria-disabled="true"on the radiogroup and each segment - Focus ring is visible on the focused segment via
focus-visiblestyling
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
- Don't use segmented-control for >5 options. WAI-ARIA Toolbar Pattern degrades — overflow on narrow viewports clips. Reach for
<x-wirekit::tabs>or<x-wirekit::select>for larger sets.
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 |
--color-wk-border |
Track border color |
--shadow-wk-sm |
Active segment pill shadow |
--size-wk-sm / md / lg |
Segment height per size variant |
--radius-wk-md |
Track border radius |
--radius-wk-sm |
Segment pill border radius |
--padding-wk-x-md |
Horizontal padding per segment |
--text-wk-sm / md |
Font size per size variant |
--transition-wk-duration |
Pill slide animation duration |
--opacity-wk-disabled |
Dimmed state when disabled |
Customization
Override defaults in config/wirekit.php:
'components' => [
'segmented-control' => ['size' => 'md'],
],