Toggle
A button that stays pressed or not.
In Brand expressive
See it live
npm run storybookWhen to use it
In Brand expressive: A mode turned on and off in place. Not as a one-off action; button does that.
- 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 Brand expressive decided and why. Each sample is the live story with that variant set.
defaultvariantNot used in Brand expressiveWhat it is for
No border; the pressed state shows as a fill.
In Brand expressive
Sections lead with media and color blocks, not toolbars; a toggle with no border is lost on vivid color.
outlinevariantUsed in Brand expressiveWhat it is for
A border, so the toggle reads as a control at rest.
In Brand expressive
A pill with a border, so the toggle reads as a control on vivid color.
defaultsizeUsed in Brand expressiveWhat it is for
The normal size.
In Brand expressive
Every toggle, at touch size: phones first.
smsizeNot used in Brand expressiveWhat it is for
A smaller toggle.
In Brand expressive
Phones first: no compact controls.
lgsizeNot used in Brand expressiveWhat it is for
A larger toggle.
In Brand expressive
Large is for the one call to action per section, and a toggle is not that action.
Why it works this way
Toggle looks the way it does because of 6 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.
components.roles1The answer
Campaigns lead with media and big sections.
2So Toggle
- –does not use default: Sections lead with media and color blocks, not toolbars; a toggle with no border is lost on vivid color.
3If it changed
No other answer follows from it.
color.values1The answer
Vivid color is the brand.
2So Toggle
- –does not use default: Sections lead with media and color blocks, not toolbars; a toggle with no border is lost on vivid color.
- ✓uses outline: A pill with a border, so the toggle reads as a control on vivid color.
Item: outline3If it changed
and 2 answers that follow from it: Two-tone, Brand color.
shape.values1The answer
Pill controls feel playful.
2So Toggle
- ✓uses outline: A pill with a border, so the toggle reads as a control on vivid color.
3If it changed
No other answer follows from it.
accessibility.values1The answer
Visitors come from social posts on phones.
2So Toggle
- ✓uses default: Every toggle, at touch size: phones first.
- –does not use sm: Phones first: no compact controls.
Button: default Button: compact Button: icon Field: horizontal Input group: inline-end Item: default Item: sm3If it changed
and 1 answer that follow from it: Those and pressed.
tasks.q21The answer
One action: sign up.
2So Toggle
- –does not use lg: Large is for the one call to action per section, and a toggle is not that action.
Alert: default Input group: block-start Input group: block-end3If it changed
and 2 answers that follow from it: Three, Only what today's screens use.
outcomes.q11The answer
Engagement and sign-ups.
2So Toggle
- –does not use lg: Large is for the one call to action per section, and a toggle is not that action.
Button: primary Button: large Badge: warning Toast: success3If it changed
and 6 answers that follow from it: A few minutes, Brand moments, Hero and launch moments, and 3 more.
Words
- Headlines are short and bold. Three to six words.
- Verbs that move: Join, Try, Go.
- Playful in headlines, plain in errors.
- Use: Join, Drop, Go, Love. Avoid: Synergy, Leverage, Kindly.
The labels components use, from the voice tokens:
content.action.commit: Gocontent.action.cancel: Not nowcontent.action.destroy: Removecontent.action.retry: Try againcontent.action.empty-action: Explore the drop
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.
Brand expressive 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.999:9999px |
| Text size | component.toggle.font-sizetype.body.font-sizevar(--toggle-font-size) | font.size.17:1.0625rem |
| 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) | Lighttaupe.light.100:#FCEFE6Darktaupe.dark.100:#33241F |
| Label hover Changes with theme on hover | component.toggle.label-hovercolor.text.mutedvar(--toggle-label-hover) | Lighttaupe.light.600:#6B544ADarktaupe.dark.400:#B5988A |
| Border focus Changes with theme on focus | component.toggle.border-focuscolor.border.focusvar(--toggle-border-focus) | Lightpink.light.600:#D61F7ADarkpink.dark.600:#F59ACA |
| Disabled opacity disabled when disabled | component.toggle.disabled-opacity-disabledopacity.disabledvar(--toggle-disabled-opacity-disabled) | opacity.40:0.4 |
| 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) | Lighttaupe.light.100:#FCEFE6Darktaupe.dark.100:#33241F |
| Label on Changes with theme when on | component.toggle.label-oncolor.text.primaryvar(--toggle-label-on) | Lighttaupe.light.900:#1F1411Darktaupe.dark.900:#FFF8F3 |
| 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) | Lightpink.light.600:#D61F7ADarkpink.dark.600:#F59ACA |
| Focus ring offset focus Changes with theme on focus | component.toggle.focus-ring-offset-focuscolor.surface.canvasvar(--toggle-focus-ring-offset-focus) | Lighttaupe.light.50:#FFF8F3Darktaupe.dark.0:#120B09 |
| Outline | ||
| Border Changes with theme | component.toggle.outline.bordercolor.border.strongvar(--toggle-outline-border) | Lighttaupe.light.500:#8A6F63Darktaupe.dark.400:#B5988A |
| Shadow Changes with theme | component.toggle.outline.shadowelevation.cardvar(--toggle-outline-shadow) | Lightshadow.cardDarkshadow.card-dark |
| Background hover Changes with theme on hover | component.toggle.outline.background-hovercolor.surface.sunkenvar(--toggle-outline-background-hover) | Lighttaupe.light.100:#FCEFE6Darktaupe.dark.100:#33241F |
| Label hover Changes with theme on hover | component.toggle.outline.label-hovercolor.text.primaryvar(--toggle-outline-label-hover) | Lighttaupe.light.900:#1F1411Darktaupe.dark.900:#FFF8F3 |
| 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.24:1.5rem |