Components › Forms & input › Choice › Switch

Switch

canon

On/off control that applies immediately.

Size sm (acrm !3911, AD 26 Sep 2026): 30×17, knob 13px, --shadow-xs, label var(--small) in --fg-secondary, gap --sp-2 — ONLY the secondary option in a dialog footer next to buttons (Další / Vytvořit další / Nevolat / Pozvánky); the full size would compete with the primary. Replaces the private .tk-more/.sw copy — migrated on 27 Sep 2026 (acrm !3950): TaskDialog „Další“, NewContactDialog „Vytvořit další“/„Nevolat“, NewMeetingForm „Pozvánky“/„Nahrávat“; .tk-more/.sw deleted, the app has ONE switch. Clickable label (acrm !3933, AD 26 Sep 2026): a click on the VISIBLE label toggles (not with labelHidden); focus stays on the role=switch button, onChange(on, event) keeps stopPropagation for clickable rows, no double toggle.

Used in 35 files · 16 areas · 52× — components/CheckMenu, components/assistant, components/datamart, components/mail, components/meetings, components/person …

Keyboard

  • Native <button>: Tab moves focus, Enter and Space toggle.
  • disabled removes it from the tab order (used while an async write runs).
  • No shortcut chip exists for the primitive.

ARIA

  • One pattern everywhere: role="switch" + aria-checked={on} on button.acrm-switch. Announced as "switch, on/off".
  • Settled 10 Sep 2026 (AD). The React <Switch> used to report aria-pressed ("toggle button, pressed") while eleven of the thirteen hand-written copies reported role="switch" (the two on DsInputsPage had no ARIA at all) — one visual control saying different things to a screen reader, and "pressed button" describes an action, not a held state. The component was moved to the majority (acrm MR !3307). Since 12 Sep 2026 no copy is left: every switch renders through <Switch> (acrm MR !3467).
  • Accessible name: the visible label is not linked to the button, so the component copies a string label into aria-label itself — visible or hidden; ariaLabel overrides it. Until 13 Sep 2026 this happened only with labelHidden, so a switch with a visible label and no ariaLabel had no name at all ("Celý den" and "Opakovat" in the meeting dialog). Fixed in the component, not per call site (acrm MR !3470), with a regression test that queries both by getByRole('switch', { name }).
  • A non-string label (mail compose footer, "Sledovat" in a <span>) still yields no name unless ariaLabel or title is passed; that switch has title.
  • .is-on is visual only; the state is aria-checked.
  • The knob <i> is empty and needs no aria-hidden.

Focus

  • .acrm-switch:focus-visible sets outline: none and box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 30%, transparent) on the track (the foundation ring for [role='switch'] in ds.css is overridden by this rule).
  • The ring follows the pill radius and does not change the track colour.

Contrast

Computed from packages/ui/src/tokens/tokens.css (WCAG 2.x relative luminance):

  • On track --accent #0072f5 on --surface-100 #ffffff = 4.44:1 (light); on #1b1c1f = 3.83:1 (dark) — above 3:1.
  • Off track --line-300 #dcdbdd on white = 1.38:1 (light), #3a3c42 on #1b1c1f = 1.55:1 (dark) — below the 3:1 non-text minimum.
  • Knob white on off track = 1.38:1 (light), 11.02:1 (dark); knob white on on track = 4.44:1.
  • Label --ink-800 #1f2023 on white = 16.29:1 (light), #e1e2e4 on #1b1c1f = 13.14:1 (dark).
  • Focus ring 30% accent mix is decorative.

Known limits

  • Off-state track (1.38:1 light) and knob edge are below 3:1; the state is readable mainly by knob position and the accent fill when on. A --border-strong hairline on the track would fix it — not in code.
  • No disabled style in inputs.css; disabled works for AT and keyboard but the track keeps full opacity.
  • The row text is not programmatically the label; the name is a copy in aria-label, so a screen reader that shows text-to-name links sees none. Wrapping row and text in a <label> would fix it — not in code.
  • Knob and check colours are literal #fff in CSS (not a token).

Source: docs/choice/switch.a11y.md · GitLab