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);}Usage
AstrGlassToggle(value: showClouds,onChanged: (bool on) => setState(() => showClouds = on),label: 'Cloud cover',tone: AstrTone.deepSpace,)astr_glass.dart · AstrGlassToggle
/// A switch. The track is a golden rectangle, 55 by 34, with a 21 thumb; when/// [compact] it steps down the Fibonacci ladder to 34 by 21 and a 13 thumb./// On, the track fills with the tone and the thumb turns dark.class AstrGlassToggle extends StatefulWidget { const AstrGlassToggle({ super.key, required this.value, required this.onChanged, required this.label, this.tone = AstrTone.deepSpace, this.compact = false, }); final bool value; /// Null disables the switch. final ValueChanged<bool>? onChanged; /// Accessible name, read by screen readers. final String label; final AstrTone tone; final bool compact; @override State<AstrGlassToggle> createState() => _AstrGlassToggleState();}API reference
| React prop | Flutter parameter | Type | Default | Meaning |
|---|---|---|---|---|
| label | label | string | required | Accessible name, drawn beside the switch unless hideLabel is set. |
| checked, onChange | value, onChanged | boolean | none | Controlled state. Flutter passes null to onChanged to disable. |
| defaultChecked | none | boolean | false | Uncontrolled initial state (web). |
| tone | tone | AstrTone | deep-space | The colour of the on track. |
| size | compact | sm / boolean | 55 by 34 | Small is 34 by 21. |
| disabled | onChanged: null | boolean | false | Not 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.
| Role | CSS token | Dart token | App widget today |
|---|---|---|---|
| Track | --spacing-f55 by --spacing-f34 (a golden rectangle), small f34 by f21 | AstrSpace.f55, f34; compact f34, f21 | new |
| Thumb | --spacing-f21, small --spacing-f13 | AstrSpace.f21, f13 | new |
| Off | --sheen over --fill, border --hair-strong, thumb --color-space-grey-300 | Mag.m5, m8; AstrColors.spaceGrey[300] | new |
| On | track the tone lightened by --mag-3, thumb --color-space-grey-950, glow like a button | tone, Mag.m3 | new |
| Motion | 0.3s, off with reduced motion | Duration(milliseconds: 300) | new |
| Disabled | opacity --mag-2 | Mag.m2 | new |
| Pressed | track glows while held | BoxShadow on tap down | new |