# Toggle

**Maturity:** experimental

A user-flippable control for a single boolean setting — the canonical glass switch.

## Import

```tsx
import { Toggle } from "@liquidify/react/toggle"
```

## Props

- `checked`: `boolean | undefined`. Controlled checked state. Passing this switches Toggle to controlled mode: the rendered state always reflects this value and the consumer owns it via {@link ToggleProps.onChange}.
- `children`: `ReactNode`. Visible label content for the `"button"` variant (the iOS `.toggleStyle(.button)` analogue). When supplied with `variant="button"`, this content renders inside the pill capsule — an icon glyph, text, or an icon+text pair — and the capsule sizes to it. Ignored — documented, not silent — for the default `"switch"` variant (a switch renders no visible text of its own). children role: `label` (charter §8).
- `className`: `string | undefined`. Optional additional CSS class merged into the glass surface class. The supported per-instance customisation seam: override any `--lq-*` custom property from this class's CSS — the token cascade the engine reads (ADR-0004). No raw inline style is involved, so this stays ADR-0003 clean.
- `defaultChecked`: `boolean | undefined`; default ``false``. Uncontrolled seed for the internal checked state (ignored once {@link ToggleProps.checked} is supplied).
- `disabled`: `boolean | undefined`. When `true`, sets the native `disabled` attribute on the `<button>`: the switch is removed from the tab order, all interaction is suppressed, and a `data-disabled` styling hook dims the surface. `aria-disabled` is NOT set — the native attribute provides full-suppression semantics.
- `indeterminate`: `boolean | undefined`; default ``false``. Tri-state DISPLAY overlay for `variant="checkbox"` (the AppKit `NSButton.allowsMixedState` / `NSControl.StateValue.mixed` lineage) — a **web-only extension** with no SwiftUI `Toggle` equivalent. When `true` the box announces `aria-checked="mixed"` (`data-state="mixed"`) and paints a dash glyph, while the underlying value contract stays boolean: `checked` / `onChange` never carry `"mixed"`. Activating a mixed box commits a concrete `onChange(true)` (the WAI-ARIA APG default). Ignored — documented, not silent — under `"switch"` / `"button"` (a switch has no tri-state).
- `labelProps`: `LabelProps | undefined`. Per-instance typography override for the `"button"`-variant label, forwarded to the internal {@link Text} (ADR-0028). Token-backed axes only; the HIG default is `variant="body"` with `color="inherit"`, so the label keeps the button's state colour (accent unchecked, on-fill checked, grey disabled). Applies only when {@link ToggleProps.children} is a string/number — an icon or icon+text element passes through unwrapped.
- `onChange`: `((checked: boolean) => void) | undefined`. Fires with the next boolean whenever the switch flips — pointer tap, drag past the midpoint, or `Space` / `Enter`. Fires at most once per effective flip, in both controlled and uncontrolled modes; a cancelled gesture, a drag settling back on its starting side, a repeated same-value request, or a disabled interaction emits nothing (the shared no-op policy).
- `onClick`: `MouseEventHandler<HTMLButtonElement> | undefined`. Documented native passthrough (charter §1): fires before Toggle's internal flip on every click event dispatched to the root — a pointer tap's trailing click, the browser's own click synthesis on `Space`, or the programmatic click Toggle dispatches on `Enter`. `event.preventDefault()` suppresses that click's flip **and** the resulting {@link ToggleProps.onChange}. A plain pointer tap defers its commit to this click event (rather than committing in the pointer handlers), and `Enter` routes through a single dispatched click, so cancellation is uniform across the pointer and keyboard (`Space` / `Enter`) paths. A drag settle (past the midpoint) is the one exception: it commits synchronously in `onPointerUp`, so its trailing click is swallowed rather than re-evaluated — documented as NOT click-cancellable. Either way `onClick` still fires exactly once per interaction (the synthesised-click swallow is preserved for drags). Toggle has no `onPress` — its activation IS the value change.
- `ref`: `Ref<HTMLButtonElement> | undefined`. Forwarded ref to the rendered element — the native `<button role="switch">` track itself (charter §4 truthful ref; there is no wrapper element to diverge from this type).
- `size`: `ToggleSize | undefined`; default ``"regular"``. Size of the switch — the `small · regular` subset of the canonical {@link SizedControlProps.sizesize scale} (the SwiftUI `.controlSize(_:)` analogue, ADR-0018). Resolved to geometry via `data-size` attribute selectors in `styles.css`; the root stamps `data-size` **unconditionally** (the Button pattern), so both values are always addressable. `mini` / `large` / `xlarge` are excluded — Apple ships no large switch.
- `tint`: `LiquidTintValue | undefined`; default ``"green"` (`"accent"` under `variant="checkbox"`)`. System-accent of the on-track (the SwiftUI `.tint(_:)` analogue), drawn from the restricted {@link LIQUID_COLORS } palette and applied via the `data-tint` attribute selector in CSS. Per Apple HIG a switch's "on" track is system green, so Toggle defaults to `"green"`. For `variant="button"` the `tint` is the button accent — the checked fill and the label colour in both checked and unchecked — following the palette (default systemBlue). Disabled greys the text but keeps the checked tint fill. For `variant="checkbox"` the HIG default is the app **`"accent"`** (system blue) — the checked/mixed box fill — diverging from the switch's green (open question O1, resolved: `"checkbox"` auto-overrides the green default unless an explicit `tint` is supplied). An explicit `tint` always wins.
- `variant`: `"button" | "switch" | "checkbox" | undefined`; default ``"switch"``. Visual style of the toggle. `"switch"` (default) renders the sliding-thumb switch; `"button"` renders a pill capsule (the SwiftUI `.toggleStyle(.button)` analogue) used for formatting toolbars and single-option on/off buttons — the whole capsule fills with the tint accent when on; `"checkbox"` renders a small rounded-square glass box (leading) with a trailing `children` label (the macOS-family `CheckboxToggleStyle` analogue) — no sliding thumb, no liquid-lens. The checkbox box carries `role="checkbox"`, a structural state glyph (checkmark / dash / empty — ADR-0019), and defaults its `tint` to the app `"accent"` (system blue) per Apple HIG, diverging from the switch green.

## SwiftUI mapping

- ``isOn: Binding<Bool>`` → ``checked` (controlled) / `defaultChecked` (uncontrolled seed) + `onChange(next: boolean)`` (direct): The controlled-value triad (ADR-0015); a `Binding` splits into the controlled `checked` + `onChange`, or the uncontrolled `defaultChecked`. `onChange` fires at most once per effective flip, in both modes — cancelled gestures, same-side settles, repeated same-value requests, and disabled interaction emit nothing (see `## Interaction`).
- ``.toggleStyle(.switch)` / `.toggleStyle(.button)` / `.toggleStyle(.checkbox)`` → ``variant: "switch" | "button" | "checkbox"`` (direct): The style-protocol name is the value (charter §6); `"switch"` is the default sliding-thumb pill, `"button"` the pill capsule, `"checkbox"` the leading glass box + trailing label (`CheckboxToggleStyle`, macOS family — no iOS render).
- ``.tint(_:)`` → ``tint`` (direct): Restricted `LiquidColor` palette; default `"green"` (HIG switch-on divergence from the base `tint="accent"` default — documented in `## Tokens`). The `"checkbox"` variant's HIG default reads as **accent** (system blue), not green — auto-override vs. caller-set is open question O1.
- ``NSButton.allowsMixedState` / `NSControl.StateValue.mixed` (AppKit, `checkbox` only)` → ``indeterminate` → `aria-checked="mixed"` + dash glyph` (web-only): **Web-only extension** — no SwiftUI `Toggle` equivalent. A controlled *display* overlay: the value contract stays boolean (`checked`/`onChange`), `indeterminate` only drives the mixed rendering + announcement. Activating a mixed box commits a concrete `onChange(true)` (APG default; the committed boolean is open question O3). Ignored — documented, not silent — under `"switch"`/`"button"`.
- ``.controlSize(_:)`` → ``size: ToggleSize` (`Extract<Size, "small" | "regular">`)` (direct): `mini`/`large`/`xlarge` excluded — Apple ships no large switch. `data-size` is stamped unconditionally on the root (Button pattern); default `"regular"`.
- ``.disabled(_:)`` → ``disabled`` (direct): Shape N: native `disabled` + `data-disabled`; no `aria-disabled` (full-suppression semantics, charter §3).
- ``Toggle(_:isOn:) { label }` closure` → ``children`` (direct): Visible label under `variant="button"` and `variant="checkbox"` (icon, text, or icon+text; trailing the box under `"checkbox"`); ignored — documented, not silent — under `"switch"` (charter §8 `children` role: label).
- `label-view modifiers (`.bold()`, …)` → ``labelProps` (`Pick<TextProps, …>`)` (direct): Per-instance typography override forwarded to the internal `Text` (ADR-0028); token-backed axes only, applies only to string/number `children`.
- ``.accessibilityLabel(_:)` / `.labelsHidden()` (nameless switch)` → ``aria-label` / `aria-labelledby`` (direct): Required accessible-name source under `variant="switch"` — the control renders no text of its own. Under `"button"`/`"checkbox"` a text `children` label supplies the name, but an icon-only/empty label still requires an explicit `aria-label` / `aria-labelledby`.
- `— (web-only)` → ``className`` (web-only): Overridable glass-surface class (`LiquidBaseProps`, inherited).
- `— (web-only)` → ``ref: Ref<HTMLButtonElement>`` (web-only): Forwarded to the native `<button role="switch">` track — the charter-truthful ref target (charter §4).
- `— (web-only)` → ``onClick?: MouseEventHandler<HTMLButtonElement>`` (web-only): Documented native escape hatch (charter §1): fires before the internal flip on every click event (pointer tap, Space); `event.preventDefault()` suppresses the flip and `onChange`. Toggle has no `onPress` — its activation IS the value change (charter §1 rule 4).
