Toggle

Two-state toggle button.

Usage

Toggle is a two-state button. Bits renders a real <button> and owns aria-pressed, so the state is announced without any work on your side. An icon-only toggle carries no text, so it needs an aria-label — otherwise it has no accessible name at all.

Variants

outline keeps a visible border when off; ghost shows nothing until hovered or pressed. Both use the primary tone for the on state.

Sizes

Three sizes matched to the Button scale, so they line up in a toolbar.

Toggle vs Switch

They are not interchangeable, and the difference is about when the change takes effect. A Switch is a setting — flipping it applies immediately, like turning on notifications. A Toggle is a button that stays pressed — bold in a text editor, a filter that is active. If the control belongs in a toolbar next to other buttons, you want Toggle. If it belongs in a settings form, you want Switch.

Need several related toggles where only one can be active? That is Toggle Group, not a row of Toggles.

Props

Plus every native <button> attribute via prop spreading. pressed is bindable.

PropTypeDefault
pressed Whether the toggle is pressed. Bindable.booleanfalse
disabled booleanfalse
size 'sm' | 'md' | 'lg''md'
variant 'outline' | 'ghost''outline'
invalid Marks the control as failing validation. Always applies the invalid styling. Does NOT set `aria-invalid`: this renders as `button`, and ARIA does not support the attribute there, so assistive technology is free to ignore it. axe does NOT flag it either way — verified by injecting it and watching the suite still pass — so this is a decision taken from the spec, not one a tool enforces. The accessible signal comes from `Field` wiring the error message through `aria-describedby`. Prefer letting `Field` drive this: passing `Field` an `error` is what makes a field invalid, so the message the user reads and the state of the control cannot disagree.booleanfalse
required Accepted and deliberately NOT forwarded. This renders as a `<button>`. A native `required` attribute is inert there, and `aria-required` is worse — axe reports it as an `aria-allowed-attr` violation, verified rather than assumed. (Note `aria-invalid` differs: axe accepts that one on a button. The two rules are not the same.) So it is swallowed here rather than emitted as dead markup. When wrapped by `Field`, the required signal a user gets is the marker on the visible label plus the error on submit — which is what `Field` is for.booleanfalse
class string
child bits-ui Snippet<[ToggleRootSnippetProps & { props: Record<string, unknown>; }]> | undefined
children bits-ui Snippet<[ToggleRootSnippetProps]> | undefined
id bits-ui string | undefined
onPressedChange bits-ui A callback function called when the toggle is pressed.OnChangeFn<boolean> | undefined
ref bits-ui HTMLElement | null | undefined