Skip to main content
WireKit
Copy for LLM

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

View Switcher

Without Preselected Value

No Default Selection

Size Variants

Three Sizes

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.

Optimistic segmented control — 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.

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" with aria-label from the label prop
  • Each segment: role="radio" with aria-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-visible styling

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'],
],

Further Reading

Was this page helpful?

Thanks — that helps.

Voting requires cookies or local storage. What we store