Skip to main content
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

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.

Many options on a narrow screen

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.

Options in their own language

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.number.live. 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.js 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.

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.

Error state

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" with aria-label from the label prop
  • Each segment: role="radio" with aria-checked="true" / "false"
  • An autofocus written 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-visible styling
  • 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-min sets 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::tabs> or <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/wirekit.php:

'components' => [
    'segmented-control' => ['size' => 'md'],
],

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