Diff: design-system/wiki-dark-mode
From 95f6beb to 95f6beb
+0 / −0 lines
| Before | After |
|---|---|
| --- | --- |
| schema: foundry-doc-v1 | schema: foundry-doc-v1 |
| title: "Wiki dark mode" | title: "Wiki dark mode" |
| slug: wiki-dark-mode | slug: wiki-dark-mode |
| short_description: "Light and dark colour schemes for the PointSav wiki, with WCAG-verified palettes and theme-persistence via localStorage." | short_description: "Light and dark colour schemes for the PointSav wiki, with WCAG-verified palettes and theme-persistence via localStorage." |
| category: design-system | category: design-system |
| type: topic | type: topic |
| status: active | status: active |
| bcsc_class: public-disclosure-safe | bcsc_class: public-disclosure-safe |
| last_edited: 2026-05-25 | last_edited: 2026-05-25 |
| editor: pointsav-engineering | editor: pointsav-engineering |
| paired_with: wiki-dark-mode.es.md | paired_with: wiki-dark-mode.es.md |
| --- | --- |
| # Wiki dark mode | # Wiki dark mode |
| The [[app-mediakit-knowledge|PointSav wiki]] supports light and dark colour schemes using [[design-system-substrate|semantic tokens]] from the platform design system. Dark mode reduces eye strain in low-light environments and is preferred by a significant proportion of readers. This article describes the implementation: how the theme is set, persisted across sessions, and toggled, together with the full colour palette for each mode. | The [[app-mediakit-knowledge|PointSav wiki]] supports light and dark colour schemes using [[design-system-substrate|semantic tokens]] from the platform design system. Dark mode reduces eye strain in low-light environments and is preferred by a significant proportion of readers. This article describes the implementation: how the theme is set, persisted across sessions, and toggled, together with the full colour palette for each mode. |
| --- | --- |
| ## How it works | ## How it works |
| Dark mode is controlled by a `data-theme="dark"` attribute on the `<html>` element. The wiki's CSS uses this attribute as a selector override: | Dark mode is controlled by a `data-theme="dark"` attribute on the `<html>` element. The wiki's CSS uses this attribute as a selector override: |
| ```css | ```css |
| /* Light (default) — defined on :root */ | /* Light (default) — defined on :root */ |
| :root { | :root { |
| --ps-surface-base: #ffffff; | --ps-surface-base: #ffffff; |
| --ps-ink-primary: #0e0f12; | --ps-ink-primary: #0e0f12; |
| /* ... */ | /* ... */ |
| } | } |
| /* Dark — overrides semantic tokens only */ | /* Dark — overrides semantic tokens only */ |
| [data-theme="dark"] { | [data-theme="dark"] { |
| --ps-surface-base: #1f2125; | --ps-surface-base: #1f2125; |
| --ps-ink-primary: #f5f6f8; | --ps-ink-primary: #f5f6f8; |
| /* ... */ | /* ... */ |
| } | } |
| ``` | ``` |
| Only semantic tokens (surfaces, ink, borders, status colours) change between modes. Primitive tokens — the raw colour palette — remain unchanged. Adding dark mode support to a new component requires only that the component uses semantic tokens; no per-component `[data-theme="dark"]` selectors are needed. | Only semantic tokens (surfaces, ink, borders, status colours) change between modes. Primitive tokens — the raw colour palette — remain unchanged. Adding dark mode support to a new component requires only that the component uses semantic tokens; no per-component `[data-theme="dark"]` selectors are needed. |
| --- | --- |
| ## Initialisation | ## Initialisation |
| Theme preference is stored in `localStorage` under the key `ps-theme`. On each page load, an inline script in `<head>` reads this value and sets `data-theme` before the browser renders any content. This prevents a flash of the wrong theme that would otherwise occur if the script ran after the initial paint: | Theme preference is stored in `localStorage` under the key `ps-theme`. On each page load, an inline script in `<head>` reads this value and sets `data-theme` before the browser renders any content. This prevents a flash of the wrong theme that would otherwise occur if the script ran after the initial paint: |
| ```html | ```html |
| <head> | <head> |
| <script> | <script> |
| (function() { | (function() { |
| var stored = localStorage.getItem('ps-theme'); | var stored = localStorage.getItem('ps-theme'); |
| var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; |
| if (stored === 'dark' || (!stored && prefersDark)) { | if (stored === 'dark' || (!stored && prefersDark)) { |
| document.documentElement.dataset.theme = 'dark'; | document.documentElement.dataset.theme = 'dark'; |
| } | } |
| })(); | })(); |
| </script> | </script> |
| <link rel="stylesheet" href="/static/tokens.css"> | <link rel="stylesheet" href="/static/tokens.css"> |
| </head> | </head> |
| ``` | ``` |
| An explicit user choice stored in `localStorage` overrides the operating-system preference (`prefers-color-scheme`). If no choice has been stored, the OS preference is honoured. | An explicit user choice stored in `localStorage` overrides the operating-system preference (`prefers-color-scheme`). If no choice has been stored, the OS preference is honoured. |
| --- | --- |
| ## Toggle component | ## Toggle component |
| The `wiki-dark-mode-toggle` component uses `aria-pressed` and updates `aria-label` to describe the action available, not the current state: | The `wiki-dark-mode-toggle` component uses `aria-pressed` and updates `aria-label` to describe the action available, not the current state: |
| - In light mode: label = "Switch to dark mode" | - In light mode: label = "Switch to dark mode" |
| - In dark mode: label = "Switch to light mode" | - In dark mode: label = "Switch to light mode" |
| On click, the toggle sets `document.documentElement.dataset.theme` and writes the new value to `localStorage`. | On click, the toggle sets `document.documentElement.dataset.theme` and writes the new value to `localStorage`. |
| --- | --- |
| ## Colour palette | ## Colour palette |
| ### Light mode | ### Light mode |
| | Token | Value | Use | | | Token | Value | Use | |
| |---|---|---| | |---|---|---| |
| | `--ps-surface-base` | #ffffff | Page background | | | `--ps-surface-base` | #ffffff | Page background | |
| | `--ps-surface-subtle` | #f5f6f8 | Sidebar, code surface | | | `--ps-surface-subtle` | #f5f6f8 | Sidebar, code surface | |
| | `--ps-ink-primary` | #0e0f12 | Body text | | | `--ps-ink-primary` | #0e0f12 | Body text | |
| | `--ps-ink-secondary` | #4a4f59 | Secondary text, metadata | | | `--ps-ink-secondary` | #4a4f59 | Secondary text, metadata | |
| | `--ps-wiki-link` | #234ed8 | Hyperlinks | | | `--ps-wiki-link` | #234ed8 | Hyperlinks | |
| | `--ps-wiki-redlink` | #a52323 | Non-existent article links | | | `--ps-wiki-redlink` | #a52323 | Non-existent article links | |
| | `--ps-wiki-code-keyword` | #7c3aed | Code syntax keywords | | | `--ps-wiki-code-keyword` | #7c3aed | Code syntax keywords | |
| ### Dark mode | ### Dark mode |
| | Token | Value | Use | WCAG contrast vs background | | | Token | Value | Use | WCAG contrast vs background | |
| |---|---|---|---| | |---|---|---|---| |
| | `--ps-surface-base` | #1f2125 | Page background | — | | | `--ps-surface-base` | #1f2125 | Page background | — | |
| | `--ps-surface-code` | #151618 | Code block background | — | | | `--ps-surface-code` | #151618 | Code block background | — | |
| | `--ps-ink-primary` | #f5f6f8 | Body text | 14.5:1 (AAA) | | | `--ps-ink-primary` | #f5f6f8 | Body text | 14.5:1 (AAA) | |
| | `--ps-ink-secondary` | #aab0bb | Secondary text | 6.2:1 (AAA) | | | `--ps-ink-secondary` | #aab0bb | Secondary text | 6.2:1 (AAA) | |
| | `--ps-wiki-link` | #6ab0f5 | Hyperlinks | 8.47:1 vs page (AAA) | | | `--ps-wiki-link` | #6ab0f5 | Hyperlinks | 8.47:1 vs page (AAA) | |
| | `--ps-wiki-redlink` | #f56565 | Non-existent article links | 6.42:1 vs page (AA+) | | | `--ps-wiki-redlink` | #f56565 | Non-existent article links | 6.42:1 vs page (AA+) | |
| | `--ps-wiki-code-keyword` | #c792ea | Code syntax keywords | 7.85:1 vs code surface (AAA) | | | `--ps-wiki-code-keyword` | #c792ea | Code syntax keywords | 7.85:1 vs code surface (AAA) | |
| All dark mode colour pairs pass WCAG 2.1 Level AAA, verified 2026-05-06. | All dark mode colour pairs pass WCAG 2.1 Level AAA, verified 2026-05-06. |
| ### Wiki surface aliases | ### Wiki surface aliases |
| The wiki CSS uses short-form aliases that map to the semantic tokens: | The wiki CSS uses short-form aliases that map to the semantic tokens: |
| ```css | ```css |
| --color-surface-page: var(--ps-surface-base); | --color-surface-page: var(--ps-surface-base); |
| --color-surface-sidebar: var(--ps-surface-subtle); | --color-surface-sidebar: var(--ps-surface-subtle); |
| --color-surface-code: var(--ps-surface-code); | --color-surface-code: var(--ps-surface-code); |
| --color-text-primary: var(--ps-ink-primary); | --color-text-primary: var(--ps-ink-primary); |
| --color-text-secondary: var(--ps-ink-secondary); | --color-text-secondary: var(--ps-ink-secondary); |
| --color-text-link: var(--ps-wiki-link); | --color-text-link: var(--ps-wiki-link); |
| --color-text-redlink: var(--ps-wiki-redlink); | --color-text-redlink: var(--ps-wiki-redlink); |
| --color-border-subtle: var(--ps-border-subtle); | --color-border-subtle: var(--ps-border-subtle); |
| --color-accent-primary: var(--ps-interactive-primary); | --color-accent-primary: var(--ps-interactive-primary); |
| --color-code-keyword: var(--ps-wiki-code-keyword); | --color-code-keyword: var(--ps-wiki-code-keyword); |
| ``` | ``` |
| --- | --- |
| ## See also | ## See also |
| - [[wiki-component-library]] — the nine components that consume these dark-mode token overrides | - [[wiki-component-library]] — the nine components that consume these dark-mode token overrides |
| - [[wiki-typography-system]] — the type stack that pairs with these colour settings | - [[wiki-typography-system]] — the type stack that pairs with these colour settings |
| - [[design-system-substrate]] — the token vault where semantic token values are defined and versioned | - [[design-system-substrate]] — the token vault where semantic token values are defined and versioned |