Skip to content

Components

Toggle

A switch for a setting that is on or off. It is a real checkbox with the switch role, so it works with the keyboard and with screen readers.

Preview

Variants

Pick a variant with the controls. The preview and both code blocks follow.

Tone
Size
On
Disabled
Variant.tsx
<AppToggle label="Cloud cover" defaultChecked />

Implementation

The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.

Usage
<AppToggle label="Cloud cover" defaultChecked /><AppToggle label="Now" tone="sodium-airglow" size="sm" />
app-toggle.tsx · AppToggle
/** * A switch for a setting that is on or off. It is a real checkbox with the switch role, so it works with the * keyboard and screen readers; the track and thumb are drawn in CSS. Pass `checked` and `onChange` to control it, or * `defaultChecked` to let it keep its own state. */export function AppToggle({  label,  hideLabel,  tone = "deep-space",  size,  checked,  defaultChecked,  onChange,  disabled,}: {  label: string;  /** Keep the label for assistive technology but do not draw it. */  hideLabel?: boolean;  tone?: Tone;  size?: "sm";  checked?: boolean;  defaultChecked?: boolean;  onChange?: (checked: boolean) => void;  disabled?: boolean;}) {  const state = checked === undefined ? { defaultChecked } : { checked };  return (    <label className={`app-ui app-toggle${size ? ` is-${size}` : ""}`} style={{ ["--tone" as string]: `var(--color-${TONE_STOP[tone]})` }}>      <input type="checkbox" role="switch" aria-label={hideLabel ? label : undefined} disabled={disabled} onChange={onChange ? (e) => onChange(e.target.checked) : undefined} {...state} />      <span className="app-toggle-track" aria-hidden="true">        <span className="app-toggle-thumb" />      </span>      {!hideLabel && <span className="app-label-md">{label}</span>}    </label>  );}
app-ui.css
.app-toggle {  --tone: var(--color-deep-space-200);  --track-w: var(--spacing-f55);  --track-h: var(--spacing-f34);  --thumb: var(--spacing-f21);  position: relative;  display: inline-flex;  align-items: center;  gap: var(--spacing-f13);  cursor: pointer;}.app-toggle.is-sm { --track-w: var(--spacing-f34); --track-h: var(--spacing-f21); --thumb: var(--spacing-f13); }.app-toggle input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: inherit; }.app-toggle-track {  position: relative;  flex: none;  width: var(--track-w);  height: var(--track-h);  border-radius: var(--radius-full);  border: var(--spacing-f1) solid var(--hair-strong);  background: var(--sheen), var(--fill);  box-shadow: var(--highlight);  transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;  box-sizing: border-box;}.app-toggle-thumb {  position: absolute;  top: 50%;  left: calc((var(--track-h) - var(--thumb)) / 2 - var(--spacing-f1));  width: var(--thumb);  height: var(--thumb);  border-radius: var(--radius-full);  background: var(--c-muted);  transform: translateY(-50%);  transition: left 0.3s cubic-bezier(0.215, 0.61, 0.355, 1), background 0.3s;}.app-toggle input:checked + .app-toggle-track {  background: linear-gradient(180deg, color-mix(in srgb, var(--c-text) var(--mag-3), var(--tone)), var(--tone));  border-color: color-mix(in srgb, var(--c-text) var(--mag-3), var(--tone));  box-shadow: var(--highlight), 0 var(--spacing-f8) var(--spacing-f21) calc(var(--spacing-f8) * -1) var(--tone);}.app-toggle input:checked + .app-toggle-track .app-toggle-thumb {  left: calc(var(--track-w) - var(--thumb) - (var(--track-h) - var(--thumb)) / 2 - var(--spacing-f1));  background: var(--c-ink);}

API reference

React propFlutter parameterTypeDefaultMeaning
labellabelstringrequiredAccessible name, drawn beside the switch unless hideLabel is set.
checked, onChangevalue, onChangedbooleannoneControlled state. Flutter passes null to onChanged to disable.
defaultCheckednonebooleanfalseUncontrolled initial state (web).
tonetoneAstrTonedeep-spaceThe colour of the on track.
sizecompactsm / boolean55 by 34Small is 34 by 21.
disabledonChanged: nullbooleanfalseNot interactive.

Design tokens

Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.

RoleCSS tokenDart tokenApp widget today
Track--spacing-f55 by --spacing-f34 (a golden rectangle), small f34 by f21AstrSpace.f55, f34; compact f34, f21new
Thumb--spacing-f21, small --spacing-f13AstrSpace.f21, f13new
Off--sheen over --fill, border --hair-strong, thumb --color-space-grey-300Mag.m5, m8; AstrColors.spaceGrey[300]new
Ontrack the tone lightened by --mag-3, thumb --color-space-grey-950, glow like a buttontone, Mag.m3new
Motion0.3s, off with reduced motionDuration(milliseconds: 300)new
Disabledopacity --mag-2Mag.m2new
Pressedtrack glows while heldBoxShadow on tap downnew