Toggle
A button that stays pressed or not.
In Accessible first
See it live
npm run storybookWhen to use it
In Accessible first: Turning a mode on and off, with a word on the button. Not a form answer or a one-off action.
- Use it when: Turning a mode or format on and off in a toolbar, such as bold.
- Not when: A setting saved at once (switch), or a one-off action (button).
Instead:
- Switch: A setting.
- Button: A one-off action.
- Toggle group: Several toggles together.
Variants
What each variant is for in any design system, then what Accessible first decided and why. Each sample is the live story with that variant set.
defaultvariantNot used in Accessible firstWhat it is for
No border; the pressed state shows as a fill.
In Accessible first
Every action looks like a control people can find, so the toggle keeps its border.
outlinevariantUsed in Accessible firstWhat it is for
A border, so the toggle reads as a control at rest.
In Accessible first
Every toggle, with a strong border and a word, so it reads as a control at any zoom.
defaultsizeUsed in Accessible firstWhat it is for
The normal size.
In Accessible first
Every toggle. Large targets help people with motor needs.
smsizeNot used in Accessible firstWhat it is for
A smaller toggle.
In Accessible first
Large targets help people with motor needs; nothing smaller.
lgsizeNot used in Accessible firstWhat it is for
A larger toggle.
In Accessible first
The default size is already large for motor needs; a bigger one adds nothing.
Why it works this way
Toggle looks the way it does because of 4 answers about this product. Each card is one answer, in three steps: the answer, what it decided for Toggle, and what would move if it changed.
human.q21The answer
Nobody remembers the interface from last time.
2So Toggle
- –does not use default: Every action looks like a control people can find, so the toggle keeps its border.
Button: secondary Button: ghost Button: disabled Field: responsive3If it changed
and 1 answer that follow from it: Actions and status.
elevation.definition1The answer
Strong borders separate groups at any zoom.
2So Toggle
- –does not use default: Every action looks like a control people can find, so the toggle keeps its border.
- ✓uses outline: Every toggle, with a strong border and a word, so it reads as a control at any zoom.
Item: default Item: outline3If it changed
No other answer follows from it.
iconography.roles1The answer
Icons support words; they never replace them.
2So Toggle
- ✓uses outline: Every toggle, with a strong border and a word, so it reads as a control at any zoom.
Button: icon Input group: inline-end3If it changed
No other answer follows from it.
accessibility.values1The answer
Large targets for tremor and touch.
2So Toggle
- ✓uses default: Every toggle. Large targets help people with motor needs.
- –does not use sm: Large targets help people with motor needs; nothing smaller.
- –does not use lg: The default size is already large for motor needs; a bigger one adds nothing.
Button: default Button: compact Button: large Item: default Item: sm3If it changed
No other answer follows from it.
Words
- Use common words. Explain any term you must use.
- One idea per sentence. Put the action first.
- Never blame the person.
- Use: Continue, Change, Start, You. Avoid: Invalid, Utilise, Submit, Error code.
The labels components use, from the voice tokens:
content.action.commit: Continuecontent.action.cancel: Go backcontent.action.destroy: Deletecontent.action.retry: Try againcontent.action.empty-action: Start a new application
See Voice and tone for the whole voice.
Accessibility and keyboard
| Keyboard | What happens | Not |
|---|---|---|
| undefined | Space or Enter presses it. | undefined |
Test before you ship:
- It says whether it is pressed. undefined
Use it in code
npx open-ds add toggle
The component reads only component keys and semantic tokens, so it follows the brand and mode on <html>.
Tokens it reads
Toggle reads 24 tokens of its own, each an alias of a semantic token. Each row is one part: its own token, the semantic it aliases, and the value it ends at. Change a semantic and every component that reads it follows; override one of Toggle's tokens to change Toggle alone.
Accessible first decides what changes when you switch theme. Here a switch changes colors only, so the marked rows differ between light and dark; everything else holds steady.
| Part | Its token and the semantic it aliases | Value |
|---|---|---|
| Toggle | ||
| Gap | component.toggle.gapspace.gap.defaultvar(--toggle-gap) | space.16:1rem |
| Corner | component.toggle.radiusradius.controlvar(--toggle-radius) | radius.6:0.375rem |
| Text size | component.toggle.font-sizetype.body.font-sizevar(--toggle-font-size) | font.size.19:1.1875rem |
| Weight | component.toggle.font-weighttype.label.font-weightvar(--toggle-font-weight) | font.weight.700:700 |
| Background hover Changes with theme on hover | component.toggle.background-hovercolor.surface.sunkenvar(--toggle-background-hover) | Lightgray.light.100:#EDEDEDDarkgray.dark.100:#222222 |
| Label hover Changes with theme on hover | component.toggle.label-hovercolor.text.mutedvar(--toggle-label-hover) | Lightgray.light.500:#595959Darkgray.dark.500:#949494 |
| Border focus Changes with theme on focus | component.toggle.border-focuscolor.border.focusvar(--toggle-border-focus) | Lightblue.light.600:#0B4FC0Darkblue.dark.600:#8AB4F8 |
| Disabled opacity disabled when disabled | component.toggle.disabled-opacity-disabledopacity.disabledvar(--toggle-disabled-opacity-disabled) | opacity.50:0.5 |
| Border invalid Changes with theme when invalid | component.toggle.border-invalidcolor.status.dangervar(--toggle-border-invalid) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| Focus ring invalid Changes with theme when invalid | component.toggle.focus-ring-invalidcolor.status.dangervar(--toggle-focus-ring-invalid) | Lightred.light.600:#C42B2BDarkred.dark.600:#F28B8B |
| Background on Changes with theme when on | component.toggle.background-oncolor.surface.sunkenvar(--toggle-background-on) | Lightgray.light.100:#EDEDEDDarkgray.dark.100:#222222 |
| Label on Changes with theme when on | component.toggle.label-oncolor.text.primaryvar(--toggle-label-on) | Lightgray.light.900:#141414Darkgray.dark.900:#F7F7F7 |
| Icon size | component.toggle.icon-sizesize.iconvar(--toggle-icon-size) | size.20:1.25rem |
| Focus on focus | component.toggle.focusborder-width.focusvar(--toggle-focus) | border-width.3:3px |
| Focus ring focus Changes with theme on focus | component.toggle.focus-ring-focuscolor.border.focusvar(--toggle-focus-ring-focus) | Lightblue.light.600:#0B4FC0Darkblue.dark.600:#8AB4F8 |
| Focus ring offset focus Changes with theme on focus | component.toggle.focus-ring-offset-focuscolor.surface.canvasvar(--toggle-focus-ring-offset-focus) | Lightgray.light.50:#F7F7F7Darkgray.dark.0:#0A0A0A |
| Outline | ||
| Border Changes with theme | component.toggle.outline.bordercolor.border.strongvar(--toggle-outline-border) | Lightgray.light.500:#595959Darkgray.dark.500:#949494 |
| Shadow | component.toggle.outline.shadowelevation.cardvar(--toggle-outline-shadow) | shadow.none |
| Background hover Changes with theme on hover | component.toggle.outline.background-hovercolor.surface.sunkenvar(--toggle-outline-background-hover) | Lightgray.light.100:#EDEDEDDarkgray.dark.100:#222222 |
| Label hover Changes with theme on hover | component.toggle.outline.label-hovercolor.text.primaryvar(--toggle-outline-label-hover) | Lightgray.light.900:#141414Darkgray.dark.900:#F7F7F7 |
| Default | ||
| Padding, left and right | component.toggle.default.padding-xcomponent-spacing-map.x2var(--toggle-default-padding-x) | space.8:0.5rem |
| Sm | ||
| Height | component.toggle.sm.heightsize.control.defaultvar(--toggle-sm-height) | size.52:3.25rem |
| Padding, left and right | component.toggle.sm.padding-xcomponent-spacing-map.x2var(--toggle-sm-padding-x) | space.8:0.5rem |
| Lg | ||
| Padding, left and right | component.toggle.lg.padding-xcomponent-spacing-map.x3var(--toggle-lg-padding-x) | space.20:1.25rem |