Skip to content

Foundations

Themes and density

Light, dark and high contrast are three sets of values for the same token names. A component never asks which theme is on; it reads the tokens and is right in all three.

Three themesLink to the section Three themes

Light

Synced2 errorsNew

Warning: Two people are editing this page

Dark

Synced2 errorsNew

Warning: Two people are editing this page

High contrast

Synced2 errorsNew

Warning: Two people are editing this page

Light is warm paper with an amber accent. Dark keeps the warmth with a lighter amber. High contrast is black with white lines and a yellow accent, for low vision and bright sunlight; text reaches 7:1 and control borders 4.5:1.

How the theme is chosenLink to the section How the theme is chosen

Set data-theme to light, dark or contrast on the root element. With no attribute, the operating system decides: prefers-contrast: more gives high contrast, then prefers-color-scheme: dark gives dark. The attribute also works on any element, which themes only that part of the page, as the previews on this site do.

Theme a whole page or one panelhtml
<html data-theme="dark">
  …
  <aside data-theme="contrast">Only this panel is high contrast</aside>
</html>

In React

ThemeProvider keeps the reader’s choice in local storage and sets the attribute. ThemeScript applies a stored choice before the first paint. ThemeToggle is the control, as a segmented group or a menu.

tsx
<ThemeProvider>
  <ThemeToggle
    label={t('theme')}
    variant="menu"
    options={{ light: t('light'), dark: t('dark'), contrast: t('contrast'), system: t('system') }}
  />
</ThemeProvider>

With Tailwind

Components never need dark:. App code that does gets a variant that follows the attribute, for dark and high contrast alike, so an app that uses it sets the attribute explicitly.

Contrast in every themeLink to the section Contrast in every theme

Computed from the token values. The tests check these pairs and more: 4.5:1 for text and 3:1 for control borders and focus in light and dark, 7:1 and 4.5:1 in high contrast.

Contrast ratio of each pair in every theme
PairLightDarkHigh contrast
Text on the pagefg on bg17.66:1AAA15.87:1AAA21.00:1AAA
Secondary text on the pagemuted on bg5.76:1AA7.15:1AAA16.83:1AAA
Secondary text on a cardmuted on surface5.86:1AA6.77:1AA16.83:1AAA
Accent text on the pageaccent-text on bg6.97:1AA10.35:1AAA14.88:1AAA
Text on a primary buttonaccent-fg on accent5.02:1AA10.00:1AAA14.88:1AAA
Success text on its tintok on ok-soft5.02:1AA7.53:1AAA10.96:1AAA
Information text on its tintinfo on info-soft5.77:1AA6.70:1AA9.88:1AAA
Warning text on its tintwarn on warn-soft5.56:1AA8.29:1AAA10.82:1AAA
Error text on its tintdanger on danger-soft5.75:1AA5.92:1AA7.67:1AAA
Text in the dark sidebarnav-fg on nav12.59:1AAA13.73:1AAA21.00:1AAA
Control border on a cardborder-strong on surface3.80:1AA, non-text3.64:1AA, non-text21.00:1AA, non-text
Focus ring on the pagefocus on bg4.94:1AA, non-text10.35:1AA, non-text14.88:1AA, non-text

DensityLink to the section Density

data-density="compact" gives the smaller controls of the desktop apps. It changes the control heights only, so text stays readable, and the compact sizes still clear the 24px target size. The density switch at the top of this site sets it on the whole page.

Comfortable, the default
Compact

Reader settingsLink to the section Reader settings

Two classes on the root carry the reader settings of the apps. a11y-reduce-motion stops motion like the system setting does, and a11y-strong-focus draws a 3px focus ring. AccessibilityProvider and AccessibilitySettings in the theme group manage them, with text size and underlined links.

See it in actionLink to the section See it in action

The theme switch in the header. The choice is shared with skribly.pl and docs.skribly.pl.

Search this site

Arrows move through the results, Enter opens one, Esc closes. Ctrl K /