ProtoWiki — theme
ProtoWiki supports light and dark themes, both based on the official Codex design tokens. Themes control colours, shadows, and a few derived visual properties. Layout is the skin's job.
How the global theme is resolved
At boot, src/theme.ts decides the global theme in this order:
?theme=lightor?theme=darkURL param, if either is present.- The stored Color preference (
config.theme:light/dark/system), set in the gallery's Appearance settings. - Otherwise (
system),window.matchMedia('(prefers-color-scheme: dark)').matchesdetermines the theme.
The result is set as data-theme="…" on <html>. When neither the URL param nor
the stored preference is pinning the value, the boot script subscribes to a
media-query listener so the global theme updates live when the OS preference changes.
Theme is platform-agnostic. It works identically in web and app prototypes —
unlike the skin (data-skin, web only) and the app OS (data-app-platform, app
only). AppChromeWrapper provides the effective theme to its subtree the same
way ChromeWrapper does; see
protowiki-app-prototyping.
How the Codex tokens get re-scoped
Codex ships two token files at :root selectors:
@wikimedia/codex-design-tokens/theme-wikimedia-ui.css (light) and
theme-wikimedia-ui-mode-dark.css (dark). Both target :root.
src/theme.ts reads both files via ?raw imports at boot,
rewrites :root to [data-theme="light"] / [data-theme="dark"],
and injects them as <style> tags. After that:
<html data-theme="light">matches the light token rule, applying--color-base,--background-color-base, etc. to the document.<html data-theme="dark">matches the dark token rule.- A descendant with
data-theme="dark"(set via a component'sthemeprop) re-applies the dark tokens to its subtree, and CSS custom-property inheritance does the rest.
src/styles/dark.css patches places where Wikipedia-shaped HTML does not
follow Codex tokens alone — RL rules with color: black, TemplateStyles with
fixed pastel backgrounds (navbox bands), etc. Most prototypes never touch this
file beyond what ships in-repo.
Wikipedia HTML: RL night class + dark.css
Article bodies mix three sources: Codex tokens (via [data-theme]), vendored
ResourceLoader CSS (src/styles/wiki-skins/), and TemplateStyles embedded
in Parsoid HTML (Module:Navbox/styles.css, Module:Infobox, …). The latter often
use raw hex (#ccf navbox titles) plus color: inherit, so foreground can track
dark tokens while backgrounds stay pastel unless we intervene.
There is no single extra Wikipedia “dark stylesheet” to fetch that replaces
TemplateStyles — those rules ship inside each /page/html response.
Instead ProtoWiki uses:
skin-theme-clientpref-nighton<html>when the global theme is dark (initTheming()keeps this in sync withdata-theme). Vector / Minerva RL bundles include selectors likehtml.skin-theme-clientpref-night .navbox afor link colours, figure backgrounds, etc.; toggling the class activates those paths without forking upstream CSS.src/styles/dark.css— rules scoped to[data-theme="dark"] .mw-parser-outputthat remap TemplateStyles surfaces (navbox row colours, infoboxcolor: blackreset, …) onto Codex design tokens.
Per-subtree dark previews (theme="dark" on a wrapper while <html> stays light)
still get (2) via data-theme on ArticleWrapper / ArticleRenderer /
ArticleLive / ArticleSnapshot / ArticleCustom; they do not
toggle the RL night class on <html>, so RL’s html.skin-theme-clientpref-night …
rules stay dormant unless you also use ?theme=dark globally.
Per-subtree theme prop
Every wrapper (and every other themable component) accepts an optional
theme prop. When set, it renders data-theme="…" on the component's
root element.
<!-- Light/dark side-by-side, no iframes needed -->
<div class="protowiki-ab">
<ChromeWrapper theme="light">
<ArticleLive article="Albert Einstein" />
</ChromeWrapper>
<ChromeWrapper theme="dark">
<ArticleLive article="Albert Einstein" />
</ChromeWrapper>
</div>
When theme is unset (the common case), nothing is rendered for the
prop and the subtree inherits from the nearest ancestor with data-theme
— ultimately from <html>.
This is what makes "show me this strip in dark mode" possible without a page reload, an iframe, or a separate route.
For a whole prototype route that should read as dark (e.g.
one where you set theme="dark" on ChromeWrapper), put theme="dark" on the outermost wrapper
(ChromeWrapper, PlainWrapper, etc.). That subtree receives the dark
token set; chrome and article both inherit. <body> may still show the
global theme behind any gaps — use ?theme=dark on the URL if you need
the document root to match for overflow / scrollbar / margins outside the
wrapper.
useTheme() — read-only hook
src/composables/useTheme.ts exports a hook that returns a reactive
read-only Ref<'light' | 'dark'> reflecting the GLOBAL theme (the value
on <html>):
import { useTheme } from '@/composables/useTheme'
const theme = useTheme() // Ref<'light' | 'dark'>, read-only
console.log(theme.value) // 'light' or 'dark'
It does not mutate page state, and it does not reflect local
subtree overrides. A component that received a theme prop already knows
its effective theme; it doesn't need this hook.
Use this only when CSS attribute selectors aren't enough — e.g., a debug overlay that displays the current global theme.
Author guidelines
- Use Codex tokens.
var(--color-base),var(--background-color-base),var(--border-color-subtle), etc. cascade through[data-theme]for free. Hand-picking hex values reintroduces the dark-mode bug you were trying to avoid. - Don't
@media (prefers-color-scheme: dark). The OS preference is already resolved intodata-themeat boot. Using attribute selectors means a localtheme="dark"override re-themes correctly without fighting the OS preference. - Use
dark.csssparingly. It's for cases where Codex tokens don't cover what you need (e.g., custom shadows on the chrome header). Most prototypes never touch this file. - Don't toggle
themefrom inside the same prototype that wants its own colour overrides. If the prototype is genuinely demonstrating a custom palette, prefer scoping styles to a class on the prototype root. Theme overrides are for staging Wikipedia-shaped prototypes.
Pitfalls
- The Codex token files target
:root. ProtoWiki rewrites them at runtime into[data-theme]selectors. If you import the token files directly in a component's<style>, you'll get always-on light or always-on dark instead of attribute-driven switching. <html>always hasdata-theme. It's set at boot. Don't write styles that depend on the absence ofdata-theme.useTheme()reflects the global state only. If a component usesuseTheme()and is also placed inside a subtree withtheme="dark", the hook returnslight(the global value) while the component renders with dark tokens. That's by design: layout queries should come from the hook; styling should come from CSS.
References
references/css-imports.md— what's pre-loaded insrc/main.ts, in what order, and why it matters for the attribute-driven cascade.