# Theme switch

The light-or-dark switch: two icon segments in one bar, the active one raised.

Block: `smbk-theme-switch`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `lg`, `md`, `sm` | `lg` |  |
| `look` | `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. |

Parts: `smbk-segmented__indicator`, `smbk-segmented__item`, `smbk-segmented__icon`

## Markup

```html
<div
  role="radiogroup"
  aria-label="Theme"
  class="smbk-theme-switch smbk-segmented smbk-segmented--size-lg smbk-segmented--look-theme"
>
  <span class="smbk-segmented__indicator" aria-hidden="true"></span>
  <label class="smbk-segmented__item">
    <input
      type="radio"
      class="smbk-segmented__control"
      aria-label="Dark theme"
      name="configure"
      value="dark"
    >
    <i
      class="ph ph-moon-stars smbk-icon smbk-icon--size-md smbk-segmented__icon"
      aria-hidden="true"
    ></i>
    <span class="smbk-tooltip__layer"><span role="tooltip" id="react-aria-theme-switch-1" aria-hidden="true" class="smbk-tooltip smbk-tooltip--arrow-top-center smbk-tooltip--inverse" hidden=""><span class="smbk-tooltip__bubble">Dark theme</span><span class="smbk-tooltip__arrow" aria-hidden="true"></span></span></span>
  </label>
  <label class="smbk-segmented__item">
    <input
      type="radio"
      class="smbk-segmented__control"
      aria-label="Light theme"
      name="configure"
      checked=""
      value="light"
    >
    <i class="ph ph-sun smbk-icon smbk-icon--size-md smbk-segmented__icon" aria-hidden="true"></i>
    <span class="smbk-tooltip__layer"><span role="tooltip" id="react-aria-theme-switch-2" aria-hidden="true" class="smbk-tooltip smbk-tooltip--arrow-top-center smbk-tooltip--inverse" hidden=""><span class="smbk-tooltip__bubble">Light theme</span><span class="smbk-tooltip__arrow" aria-hidden="true"></span></span></span>
  </label>
</div>
```

## React

```tsx
import { ThemeSwitch } from './design-system/components/theme-switch/theme-switch.js';

<ThemeSwitch size="lg" look="theme" name="configure" />
```

## Stylesheet

```css
/* 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

```json
{
  "space": {
    "inset": {
      "2xs": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Padding inside a component, smallest."
      },
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    },
    "inline": {
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      },
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      }
    },
    "text": {
      "label": {
        "$type": "dimension",
        "$value": "{{space.0}}",
        "$description": "Between a label or title and the line of supporting text right under it, inside a component. None: untrimmed line height already spaces the two lines."
      }
    }
  },
  "size": {
    "control": {
      "lg": {
        "$type": "dimension",
        "$value": "{{size.48}}",
        "$description": "Height of a large control: 48 comfortable, 40 compact. A page may use it for its main call to action; apps use md. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.32}}",
        "$description": "Height of a small control: 32 comfortable, 28 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Icon box, large."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "color": {
    "border": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      },
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "surface": {
      "canvas": {
        "$type": "color",
        "$value": "{{color.grey.200}}",
        "$description": "Page background."
      },
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      }
    },
    "text": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      },
      "link-hover": {
        "$type": "color",
        "$value": "{{color.grey.950}}",
        "$description": "Link text on hover."
      },
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      },
      "brand": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Brand-red text."
      }
    }
  },
  "radius": {
    "control": {
      "lg": {
        "$type": "dimension",
        "$value": "{{radius.8}}",
        "$description": "Corners of controls, large."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{radius.6}}",
        "$description": "Corners of controls, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{radius.4}}",
        "$description": "Corners of controls, small."
      }
    }
  },
  "typography": {
    "body-md": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "16px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    },
    "body-xs": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "12px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "elevation": {
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  },
  "motion": {
    "feedback": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.150}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    },
    "state": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.200}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.cubic-in-out}}"
      }
    }
  }
}
```

## Purpose

The control a reader uses to choose the light or the dark theme. It is the [segmented control](segmented-control.md) in its theme look, with two icon-only items, a moon and a sun, drawn small enough to live in a header.

## 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.

## 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](segmented-control.md) with labels.

## 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. |

## 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.
