The light-or-dark switch: two icon segments in one bar, the active one raised.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
sizesmbk-segmented--size-<value> | lgmdsm | lg | |
looksmbk-segmented--look-<value> | theme | theme | The segmented control's theme look: square items, each icon a step larger than the standard bar's at its size, an icon hovered in the brand text color. |
Examples
Usage
Two radios drawn as one bar: dark and light. Pointing at an icon or focusing it shows its name in a tooltip.
Sizes
Three sizes.
Dark chosen
The chosen theme is raised.
Guidelines
Purpose
The control a reader uses to choose the light or the dark theme. It is the segmented control in its theme look, with two icon-only items, a moon and a sun, drawn small enough to live in a header.
When to use
- Once per site, in the header or the footer, wherever the reader expects to find it.
When not to use
- For any other two-way choice. Use the segmented control with labels.
Usage rules
Do
- Reflect the current theme in it on load, including a system preference the page has resolved.
- Set the theme with
applyTheme()from the switch's callback, so a change looks the same on every site. - Call
followThemeColor()once where the page sets its theme, so the phone's bar matches the page.
Don't
- Offer it more than once on a page.
- Write a theme-color tag by hand: its color would be typed in, and it would not follow the theme.
- Give another part of the page its own view transition name while the theme changes: the wipe draws the page as one piece, and a part with its own name moves on its own instead.
Offer one theme switch per page, in the header or app bar, so there is one place to change the theme.
Two switches on one page can disagree and leave the reader unsure which one counts.
Anatomy
- The block, the bar:
smbk-theme-switch, on the segmented control's block with its theme look modifier, drawn as the tabs' bar is, a canvas well with a hairline border at the large, medium or small control height, its items the smallest inset in from its edge and apart. - The indicator part, first in the bar: the active item's raised surface as one piece, which slides between the two. Drawn only once the wrapper has placed it.
- Two item parts, dark first, each a hidden radio (the segmented control's control class) and an icon part, as tall as the bar inside its inset and edge and about as wide as tall. The icon is a step larger than the standard segmented control's at the same size. The active item is raised on the raised surface with a highlight edge: by the indicator, or by its own surface where there is no indicator.
Behavior
Hover colors an icon with the brand text, as Figma's theme item draws it. Pointing at an icon or moving the keyboard focus to it shows its name in a tooltip under it, which stays while pointed at and goes on Escape. Choosing calls back with the theme and the item pressed, and the raised surface slides to the chosen item with the state motion while the icons change color with the feedback motion; a change of size lands at once, and under reduced motion the raised surface lands at once while the color still fades. Setting the theme attribute on the page element, and remembering the choice, belong to the page: the theme is a reader's decision recorded on the page, not the control's state.
applyTheme(), exported beside the switch, sets that attribute with the theme wipe: the page in the new theme is drawn over the old in a circle that grows from the item pressed until it covers the far corner of the window, with the theme wipe motion, and nothing of it stays once it ends. Its background goes with it, so the whole page changes, not only its parts. An item out of sight, such as one in a closed menu, wipes from the top corner at the end of the line instead. Under reduced motion, in a browser without view transitions, or in a tab that isn't shown, the theme changes at once. The page passes it the theme and the item from the switch's callback; a press during a wipe starts the next one.
The browser's own bar on a phone (Chrome's on Android, an installed web app's title bar) takes its color from a theme-color tag, which cannot read a token. followThemeColor(), exported beside the switch, writes that tag from the page canvas token (or another color token it is given) and keeps it in step with the theme. The page calls it once, where it sets its theme.
The slide needs the React wrapper, which places the raised surface after each change. In markup written by hand, the chosen item draws its own raised surface and nothing slides.
On a touch screen the switch keeps its size, and each item takes an invisible press area out to the comfortable touch target above and below it and halfway to the other item.
Accessibility
What it does for you
- It is a radio group named "Theme"; each item has an accessible name saying which theme it sets, since the icons carry no text.
- The tooltip shows that name and is hidden from assistive technology, so it is not read twice.
- Arrow keys move between the two; the choice takes effect at once.
- On a touch screen each item takes an invisible press area out to the comfortable touch target.
What you must do
- Give the page's own words for everything it says by itself, such as the group's name and each theme's, which keep their English until you do; a page in another language can change them.
How to test
- Tab to the switch: focus lands on the chosen item with its focus ring, in both themes and in a forced-color mode. Press an arrow key: the other theme is chosen at once.
- Point at an icon or move the keyboard focus to it: its name shows in a tooltip, and Escape dismisses it.
- With a screen reader, it is read as a radio group named "Theme", each item by the theme it sets, read once.
- On a touch screen, a press just above or below an icon still chooses it.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the chosen item, and on from the group. |
| Arrow keys | Move to the other item and choose its theme at once. |
| Escape | Dismisses the tooltip showing an item's name. |
Source
The files the theme switch is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-theme-switch — Theme switch. Figma "ThemeSwitch" 4209:19857 and "ThemeItem" 4207:19190.
* The theme switch is the segmented control in its theme look, smbk-segmented smbk-segmented--look-theme, drawn by
* segmented-control/segmented-control.css: the bar, the square items, the larger icons, the brand hover, the sliding
* indicator, the touch areas and the forced colors are all there. The switch's own class is only its mark in markup.
*
* This file holds the theme wipe: applyTheme(), beside the switch, changes the page's theme inside one of the
* browser's view transitions and marks <html> with data-theme-wipe while it runs. The browser's own cross-fade is
* turned off for it, so only the circle moves, and only a view transition the wipe started is touched. The page in
* the new theme starts as a circle of nothing at the pressed option (--smbk-theme-switch-wipe-x and -y, in pixels
* from the window's corner); the script grows it with the theme wipe's motion, since how far it must reach at each
* moment depends on the window. */
html[data-theme-wipe]::view-transition-old(root),
html[data-theme-wipe]::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
html[data-theme-wipe]::view-transition-new(root) {
clip-path: circle(0 at var(--smbk-theme-switch-wipe-x) var(--smbk-theme-switch-wipe-y));
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.inset.2xs--smbk-space-inset-2xs | 2px | Spacing | |
| size.control.lg--smbk-size-control-lg | comfortable: 48pxcompact: 40pxchanges with density | Size | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| radius.control.lg--smbk-radius-control-lg | 8px | Radius | |
| color.surface.canvas--smbk-color-surface-canvas | light: #e7ebf0dark: #111213changes with theme | Color | |
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| space.inset.xl--smbk-space-inset-xl | comfortable: 16pxcompact: 12pxchanges with density | Spacing | |
| radius.control.md--smbk-radius-control-md | 6px | Radius | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.body-md--smbk-typography-body-md-font-family--smbk-typography-body-md-font-size--smbk-typography-body-md-font-weight--smbk-typography-body-md-line-height--smbk-typography-body-md-letter-spacing--smbk-typography-body-md-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 16pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.text.link-hover--smbk-color-text-link-hover | light: #111213dark: #f3f5f8changes with theme | Color | |
| color.surface.raised--smbk-color-surface-raised | light: #f9fafbdark: #232527changes with theme | Color | |
| color.border.rim--smbk-color-border-rim | light: #ffffffe6dark: #ffffff0dchanges with theme | Color | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| elevation.control--smbk-elevation-control | light: 0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430ddark: 0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a | Elevation | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| color.text.disabled--smbk-color-text-disabled | light: #7c8692dark: #606a75changes with theme | Color | |
| size.icon.md--smbk-size-icon-md | 20px | Size | |
| size.control.md--smbk-size-control-md | comfortable: 40pxcompact: 32pxchanges with density | Size | |
| space.inset.lg--smbk-space-inset-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| size.control.sm--smbk-size-control-sm | comfortable: 32pxcompact: 28pxchanges with density | Size | |
| space.inline.xs--smbk-space-inline-xs | 4px | Spacing | |
| space.inset.md--smbk-space-inset-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| radius.control.sm--smbk-radius-control-sm | 4px | Radius | |
| typography.body-sm--smbk-typography-body-sm-font-family--smbk-typography-body-sm-font-size--smbk-typography-body-sm-font-weight--smbk-typography-body-sm-line-height--smbk-typography-body-sm-letter-spacing--smbk-typography-body-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| size.icon.sm--smbk-size-icon-sm | 16px | Size | |
| space.text.label--smbk-space-text-label | 0px | Spacing | |
| typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| size.icon.lg--smbk-size-icon-lg | 24px | Size | |
| color.text.brand--smbk-color-text-brand | light: #cc0d0ddark: #f34336changes with theme | Color | |
| motion.feedback.duration--smbk-motion-feedback-duration | 150ms | Motion | |
| motion.feedback.easing--smbk-motion-feedback-easing | cubic-bezier(0.16, 1, 0.3, 1) | Motion | |
| motion.state.duration--smbk-motion-state-duration | 200ms | Motion | |
| motion.state.easing--smbk-motion-state-easing | cubic-bezier(0.65, 0, 0.35, 1) | Motion | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size |
Manifest
{
"name": "theme-switch",
"block": "smbk-theme-switch",
"drawnBy": "smbk-segmented",
"summary": "The light-or-dark switch: two icon segments in one bar, the active one raised.",
"adapt": "css",
"axes": {
"size": {
"values": [
"lg",
"md",
"sm"
],
"default": "lg"
},
"look": {
"values": [
"theme"
],
"default": "theme",
"description": "The segmented control's theme look: square items, each icon a step larger than the standard bar's at its size, an icon hovered in the brand text color."
}
},
"flags": {},
"parts": [
"indicator",
"item",
"icon"
],
"figmaNodeId": "4209:19857"
}