Skip to main content
Copy for LLM

Toggle Button

A button that stays pressed: bold, italic, mute, pin. One control, two states, aria-pressed doing the talking.

WireKit has three two-state controls and they are not interchangeable:

Use Component
A form setting the user saves ("Email notifications: on") Toggle — a role="switch" with a label
Pick one of several options Segmented Control
A single control that stays pressed Toggle Button

Basic Usage

Click to toggle

Click either button: it flips between pressed and unpressed. The pressed state is carried by the surface and an edge: filled when pressed, with a line in the muted text color just inside its border, and outline when not. The edge stands at 3:1 or more against the fill in every preset, light and dark, so the state never depends on a tint alone.

These demos use self-toggle, which lets the button manage its own state so it works with no wiring. In a real app you usually want the controlled form below instead, where the pressed state lives in your component.

In a toolbar

Toggle buttons are what a formatting bar is made of.

A formatting cluster

Icons, Tooltips and State Labels

A toggle with only an icon is named by its tooltip, which also shows on hover and focus. The tooltip does not describe the button as well, so a screen reader reads the name once:

Icon-only with a tooltip

on-icon and off-icon give each state its own glyph. Both icons stay in the button, stacked in one place, so it keeps its width as they trade places:

An icon per state

active-intent colors the icon while the toggle is pressed: accent (the default), success, warning, danger or info. primary is accepted as accent, the color it names in every other component. The pressed surface changes as well, so the color is never the only sign of the state:

Active color

on-label and off-label give each state its own words. The label then says the state, so the button renders without aria-pressed, and its name is whichever label is showing:

A label per state

Livewire

The component is controlled: the pressed state lives in your component, not in the button. That is deliberate — the truth about "is this text bold" belongs to the document.

{{-- 1. Bind the state, handle the click — no local JS state to drift --}}
<x-wirekit::toggle-button :pressed="$bold" wire:click="$toggle('bold')" aria-label="Bold">
    B
</x-wirekit::toggle-button>
// 2. One property is the whole state
public bool $bold = false;

wire:model binds a boolean property to the pressed state in both directions, with no click handler of your own:

{{-- 1. The click flips $starred, and a change to $starred on the server flips the toggle --}}
<x-wirekit::toggle-button icon="star" tooltip="Starred" wire:model.live="starred" />

wire:model and optimistic both write the pressed state, so use one of them. With both set, optimistic is kept, wire:model is dropped, and the log says so.

In an Alpine component of your own, x-model on the tag binds the pressed state the same way. See An x-model on a WireKit component.

Optimistic UI

Pass the name of the Livewire method the button should call and the pressed state flips straight away, while it goes:

<x-wirekit::toggle-button :pressed="$bold" optimistic="toggleBold">Bold</x-wirekit::toggle-button>

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 toggle button — accepted, refused, and a slow answer

Accepted

The press is kept and nothing is said.

Refused

The press is taken back, and said out loud.

Slow to answer

The dashed outline is the provisional state.

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 refusal puts the state back, because a pressed state is a discrete choice: returning to it costs you nothing, it is simply the other option.

One thing to know before you opt in. The announcement needs an element of its own, and this component is the button — so with optimistic set, a wrapper is added around it. It uses display: contents, so the layout is unchanged and the button keeps its position in any flex or grid row. What does change is the element tree: the button is no longer a direct child of its container, so a selector like .toolbar > button (or :first-child, +, ~) does not match it. Without the prop, nothing is added.

optimistic and selfToggle do not combine — the optimistic layer performs the flip, so a local one would fight it. Setting both lets the layer win.

Props

Prop Type Default Description
pressed bool false The two-state truth; drives aria-pressed and the surface. Also the initial state in self-toggle mode
selfToggle bool false Let the button flip its own state on click, with no wiring (attribute: self-toggle). Handy for a standalone toolbar; leave it off for the controlled Livewire form above
size string 'md' Forwarded to the underlying button
icon string|null null An icon shown in both states
onIcon string|null null The icon while pressed. Set together with offIcon
offIcon string|null null The icon while not pressed. Set together with onIcon
tooltip string|null null A hint on hover and focus. On a toggle with no visible label it is also the accessible name
activeIntent string 'accent' The pressed icon's color: accent, success, warning, danger or info; primary is taken as accent
onLabel string|null null The label while pressed. Set together with offLabel; the toggle then renders without aria-pressed
offLabel string|null null The label while not pressed. Set together with onLabel
scope string|null null Scoped personalization name
optimistic string|null null Livewire method to call on click, showing the new pressed state before the server agrees. A refusal puts the old state back. Adds a wrapper element — see Optimistic UI.
optimisticArgs array [] Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to.

Everything else — wire:click, aria-label, disabled, classes — passes through to the underlying Button.

Accessibility

  • The control is a real <button type="button"> with aria-pressed — the WAI-ARIA toggle-button pattern. Assistive technology announces "pressed" / "not pressed" without any extra wiring.
  • Never color-only: pressed switches the surface (filled vs outline), so the state is visible in monochrome and high contrast too (WCAG 1.4.1).
  • An icon-only toggle needs a name: a tooltip, which also names it, or an aria-label. A glyph is not a name, and a tooltip alone would not reach a touch screen, where nothing hovers.
  • With on-label and off-label there is no aria-pressed. The WAI-ARIA button pattern says a toggle's label must not change with its state, and that a label which does change makes aria-pressed unnecessary: the words already say the state. The name is the label that is showing.
  • The icons are decorative (aria-hidden). Under forced colors, where every background is replaced, a pressed toggle fills with the system selection color so the state stays visible, and its focus outline takes the system text color, so a pressed toggle that has focus still shows it.
  • It composes Button, so the focus ring, disabled model and sizes are the same ones used everywhere else.

Keyboard Interaction

Key Action
Tab Focus the button
Enter / Space Toggle the pressed state

Pitfalls

  • Do not use it for a saved setting. That is Toggle, which is a labeled form switch.
  • Do not use it for one-of-many. That is Segmented Control.
  • Do not leave an icon-only toggle unnamed. In debug it throws; in a request it logs a warning.
  • Do not add an aria-label to a toggle with on-label and off-label. It would name the button with words that are not on it (WCAG 2.5.3 Label in Name), so it is dropped and a warning is logged.

Design Tokens

The pressed and unpressed surfaces come from Button's filled and outline surfaces on the neutral intent — so a theme change restyles it with everything else. The pressed edge is --color-wk-text-muted, drawn --border-wk-width wide and never under 1px.

Further Reading

Updated in WireKit v2.67.0 (2026-10-15)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store