Theming
A theme swaps token values, never token names. tenet-ui ships two themes, light and dark, and 29 tokens change between them: every fgColor, bgColor and borderColor token, plus shadow-focus. Every other token (space, type, radius, sizes) is the same in both. Components and app code reference the name, so switching the theme restyles everything without touching a line of component code.
- Layer
- Foundation
- Rules
- 5 numbered
- Examples
- 5 do and don’t
- Updated
- Source
- theming.md
data-color-mode="light"
Autumn issue
PublishedUpdated 3 minutes ago
- --bgColor-default
- #ffffff
- --fgColor-default
- #2a241b
- --fgColor-accent
- #a23e1f
- --borderColor-default
- #ddd2bf
data-color-mode="dark"
Autumn issue
PublishedUpdated 3 minutes ago
- --bgColor-default
- #1a1712
- --fgColor-default
- #f4eee3
- --fgColor-accent
- #e9a487
- --borderColor-default
- #3d3528
How it works
Section titled “How it works”The theme is the data-color-mode attribute. Set it on <html>:
<html data-color-mode="dark">tenet-ui/styles.css defines the light values on :root, so a page without the attribute is light, and redefines the 29 themed tokens under [data-color-mode="light"] and [data-color-mode="dark"].
| Token | Light | Dark |
|---|---|---|
--bgColor-default | #ffffff | #1a1712 |
--fgColor-default | #2a241b | #f4eee3 |
--fgColor-accent | #a23e1f | #e9a487 |
--borderColor-default | #ddd2bf | #3d3528 |
--shadow-focus | 0 0 0 3px #f5e4db | 0 0 0 3px #3a2117 |
In the source, the themed tokens sit under themes.light and themes.dark with identical paths. The build drops the themes.<theme> part of the path, so themes.dark.fgColor.accent becomes --fgColor-accent, the same name as in light.
- The theme is set once, on
<html>.Dialog,Menu,Select,Popover,DatePicker,TooltipandToastrender intodocument.body, so a theme set on an inner element does not reach them. - Code never reads
data-color-modeto pick a colour, and never branches on the theme in JavaScript. It uses the token and lets the theme resolve it. - tenet-ui does not follow the operating-system preference on its own. If the app wants it, the app sets the attribute from
prefers-color-scheme:const dark = window.matchMedia('(prefers-color-scheme: dark)');const apply = () => (document.documentElement.dataset.colorMode = dark.matches ? 'dark' : 'light');apply();dark.addEventListener('change', apply); - tenet-ui does not set the CSS
color-schemeproperty. Apps that want native scrollbars and form controls to match setcolor-scheme: lightorcolor-scheme: darknext to the attribute. - Contrast is verified per theme. The AA pairs in Color are checked against both themes’ values at build time, and the build fails if a pair drops below its rating. Overriding a themed token in app CSS skips that check, so app code does not redefine
--fgColor-*,--bgColor-*or--borderColor-*.
Not supported
Section titled “Not supported”Stated so that nobody assumes otherwise:
- High-contrast themes. There is no high-contrast light or dark theme.
- Forced-colors mode (Windows High Contrast). Not tested. Field focus is drawn with
border-colorandbox-shadow, which forced-colors mode overrides, so the field focus indicator is likely to disappear there. See Accessibility. - Density modes. Spacing and control sizes are the same everywhere; there is no compact or comfortable mode.
- Brand or custom themes. Only the two shipped themes have verified contrast pairs.
Do / Don’t
Section titled “Do / Don’t”Do
<html data-color-mode="dark">One attribute, at the root
border: var(--borderWidth-thin) solid var(--borderColor-default);The name is the same in both themes
Don’t
color: theme === 'dark' ? '#f4eee3' : '#2a241b'Branching on the theme; use var(--fgColor-default)
<section data-color-mode="dark"><Menu /></section>The menu portals to document.body and renders in the page’s theme
:root { --bgColor-default: #fafafa; }Redefining a themed token skips the contrast check