DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Switch

An instant boolean toggle — flips a setting on or off the moment it is pressed

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Switch } from "@tessinaui/ui";
<Switch label="Enable notifications" defaultChecked />

When to use

A switch applies immediately — no save step, no submit moment. If a physical light switch would work for the action, a Switch works here.

SituationReach for
Instant on/off setting (notifications, dark mode as a boolean, feature flags)Switch
Choice collected now, applied on submitCheckbox
Several related options, or an indeterminate/mixed stateCheckbox (group / tree)
Two opposing options where neither is "off" (Monthly ⇄ Yearly)SegmentedControl
Three or more states (System / Light / Dark)Radio or SegmentedControl
A one-shot actionButton — a switch is never a button

Label content: a noun or short noun phrase describing the thing controlled ("Marketing emails"), sentence case, no terminal punctuation. Never phrase it as a command ("Turn on emails"), a question, or — critically — text that changes with the state: the APG requires the label to stay identical whether the switch is on or off. When you want a visible On/Off word, that is statusLabel, not the label.

Examples

Default

A basic switch with a label, toggled on by default. The label is a wrapping <label>, so clicking it toggles the switch and the whole row is one tap target.

With Description

Pair a label with supporting description text. The description reaches assistive tech through aria-describedby, never the accessible name.

Sizes

Five sizes, xs through xl — track heights 16/20/24/26/28px at a constant 2:1 aspect. The invisible hit area keeps every size at ≥44px on both axes.

Rounded

Every painted corner follows rounded proportionally — the knob's inner radius steps with the track, and square switches get square skeleton bars.

Label Placement

Logical sides: end (default) or start — both flip correctly under dir="rtl". The physical labelPosition is deprecated.

Settings rows

fullWidth + labelPlacement="start" is the mobile settings-row: text leading, control trailing, the whole row one tap target. Rich labels can carry a Badge; pass aria-label to keep the accessible name clean when they do.

Master toggle

A pause-all parent above child switches. Flipping the master disables the children instead of rewriting their values, so their choices survive. role="switch" has no mixed state — a master switch is strictly boolean.

States

Disabled (token swap, out of the interaction), read-only (full-color, focusable, not operable) and showIcon={false}.

Loading

isLoading swaps the thumb icon for a spinner, reports aria-busy and refuses toggling the read-only way — the switch stays focusable and is never announced disabled. Flip the visible state only after the operation resolves.

Async failure and retry

The full async contract: pending → failure reverts the switch, explains itself in errorMessage (announced politely) and offers a retry.

Validation

errorMessage replaces the description while shown, tints the message and sets aria-invalid. Switches rarely validate — they apply immediately — so reserve this for async failures and switches inside genuinely validated forms.

State words in the track

stateLabels renders ON/OFF words inside the track (custom { on, off } strings for i18n). Rendered at md and larger only — smaller tracks have no legible room — with contrast-audited inks on both fills.

Status word beside the track

statusLabel is the Carbon/Primer anatomy: an On/Off word OUTSIDE the track that updates with state. It is aria-hidden — the switch role already announces the state, so the word would otherwise be spoken twice.

Native form submission

name/value submit through the hidden input when on; uncheckedValue sends an explicit value when off; required runs native constraint validation.

In a table column

The column header names the axis, not each control — give every switch its own aria-label.

Conditional disclosure

A switch revealing its dependent controls. Keep the revealed block right after the row in DOM order so focus and reading order stay linear.

Selection cards

The switch's own wrapping label is the card — one tap target, no extra click handlers (Base UI 1.3.0 bubbles hidden-input clicks to ancestors, so an ancestor onClick would double-fire).

RTL

Logical properties end to end: placement and knob travel flip with dir.

Skeleton

SwitchSkeleton mirrors the configured layout — same size, same text lines, same box — so the swap causes no layout shift.

API Reference

Switch

Extends Base UI Switch.Root props (checked, defaultChecked, onCheckedChange(checked, eventDetails), disabled, readOnly, required, name, form, value, uncheckedValue, inputRef, …). The root renders a <span> plus a hidden <input>; the ref element type is HTMLElement.

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"Track 16–28px tall at 2:1; hit area ≥44px at every size
rounded"none" | "sm" | "md" | "lg" | "full""full"Track + knob corner shape, proportional (§2)
labelReactNode—Accessible name via a label text span (aria-labelledby). Rich content allowed; nested links work
descriptionReactNode—Helper below the label, wired via aria-describedby. Hidden while errorMessage is shown
errorMessageReactNode—Replaces the description; error-tinted, aria-invalid, polite live region
labelPlacement"start" | "end""end"Logical label side
fullWidthbooleanfalseText column grows, track sits at the far edge (settings row with labelPlacement="start")
showIconbooleantrueCheckmark (on) / X (off) in the knob, contrast-audited
stateLabelsboolean | { on: string; off: string }—ON/OFF words inside the track, md+ only; knob slims to half-width
statusLabelboolean | { on: ReactNode; off: ReactNode }—On/Off word beside the track, aria-hidden, updates with state
isLoadingbooleanfalseSpinner in the knob, aria-busy, read-only semantics (focusable, toggle refused)
readOnlybooleanfalseFull-color display, focusable, not operable
requiredbooleanfalseNative constraint validation + asterisk after the label
optionalbooleanfalseMuted "(optional)" marker (joins the name with a space). Mutually exclusive with required
visuallyHiddenLabelbooleanfalseHides the label visually; name AND description stay announced
dir"ltr" | "rtl"—Text direction for the wrapper
labelClassNamestring—Override the label's text classes

Deprecated aliases (dev warning, removed next minor): labelPosition ("left"/"right" → labelPlacement "start"/"end"), labelSpacing ("spread" → fullWidth), isLabelHidden (→ visuallyHiddenLabel), loading (→ isLoading).

SwitchSkeleton

Mirrors the real component's configured layout — same box for the same props. Bars are one line box of the real text ladder and follow rounded proportionally.

PropTypeDefaultDescription
size / roundedsame as Switch"md" / "full"Track box + corners
showLabelbooleantrueLabel bar
showDescriptionbooleanfalseDescription bar
showStatusLabelbooleanfalseStatus-word bar beside the track
labelPlacement"start" | "end""end"Side of the text bars (labelPosition deprecated)
fullWidthbooleanfalseMirrors the real fullWidth row
labelChars / descriptionCharsnumbersize-scaledBar lengths in characters

data-slot targets

switch (wrapper) · switch-row (wrapping label) · switch-track · switch-thumb · switch-label · switch-description · switch-error · switch-status · switch-skeleton.

Keyboard

KeyAction
Tab / Shift+TabMove focus to / away from the switch
SpaceToggle
EnterToggle — the APG lists Enter as optional for switches; Base UI ships it and Radix documents it. (Checkbox deliberately does NOT toggle on Enter; the divergence is per-pattern, not an inconsistency.)

Do / Don't

  • Do write the label as the thing controlled: "Marketing emails".
  • Do flip the visible state only after an async operation resolves; on failure revert, explain in errorMessage, offer a retry.
  • Do explain a locked/forced setting in the description (plan-gated, policy-managed) instead of leaving a bare disabled switch.
  • Don't change the label text with the state ("Enabled" ⇄ "Disabled") — APG: the label must not change when the state changes. Use statusLabel.
  • Don't use a switch to pick between two opposing options (Monthly ⇄ Yearly) — the role announces on/off, which is meaningless there. Use SegmentedControl.
  • Don't put a switch behind a Save button unless the form genuinely defers — a switch reads as instant.
  • Don't hand-roll an On/Off word next to the track — statusLabel carries the aria-hidden wiring.

Accessibility

  • role="switch" with aria-checked on the Base UI root; state also lives on a hidden <input type="checkbox"> for forms.
  • The root is a <span>, which is not a labelable element — the visible label is therefore a wrapping <label>, and the accessible name points at the label text span via aria-labelledby (a reference to the wrapping label itself hits Chromium's accname cycle guard and resolves to an empty name).
  • Space and Enter both toggle (Enter is APG-optional; see Keyboard).
  • Touch target ≥44×44px at every size via an invisible centred hit area.
  • stateLabels/statusLabel words are aria-hidden; the role announces the state, so text would double-speak.
  • isLoading reports aria-busy and keeps the switch in the Tab order.
  • Disabled uses a token swap, not opacity; Windows High Contrast maps checked state to SelectedItem system colors.
  • Track, knob and word transitions are disabled under prefers-reduced-motion.
  • Known platform quirks (not specific to this library): Firefox + VoiceOver may not re-announce the state change (correct in Chrome/Safari), and some AT historically mis-handled span-based switches. Verified with VoiceOver locally; NVDA and JAWS are untested here.
  • Group related switches with <fieldset>/<legend> (see Fieldset) or role="group" + aria-labelledby.
SurfaceTable

On this page

PlaygroundInstallationUsageWhen to useExamplesDefaultWith DescriptionSizesRoundedLabel PlacementSettings rowsMaster toggleStatesLoadingAsync failure and retryValidationState words in the trackStatus word beside the trackNative form submissionIn a table columnConditional disclosureSelection cardsRTLSkeletonAPI ReferenceSwitchSwitchSkeletondata-slot targetsKeyboardDo / Don'tAccessibility