Color
Two grounds whose roles swap. The colour that is the page in one mode is the ink in the other, so contrast is symmetric and one rule set covers both.
How it works
Outword has a dark ground (#202845, the default) and a light ground (#F2F0EA), and the roles swap between them: the ink of one is the page of the other. Two semantic hues, a green and a rose, carry meaning and are the only colours that don't invert. One more hue, a blue, is reserved for the focus ring.
The token layer has three tiers:
- Base values:
--dk-*and--lt-*, the real hex and rgba values, one per role per mode. These are the only literals in the system. - Active roles:
--ground,--fg,--tile… Components use these and nothing else. By default each points at its--dk-*value. - Mode swap:
:root[data-mode="light"]points every active role at its--lt-*value. Flipping that one attribute is the entire light/dark mechanism.
Tokens
| Token | Dark | Light | Role |
|---|---|---|---|
--ground | #202845 | #F2F0EA | the page |
--raise | rgba(242,240,234,.08) | #FAF9F6 | anything sitting on the page (cards, fields, rows) |
--raise-solid | #313852 | #FAF9F6 | opaque twin of --raise, for the crown's halo where a translucent fill can't cover what's behind it |
--fg | #F2F0EA | #202845 | primary text / primary fill |
--fg-soft | #A5ABC4 | #5F6987 | secondary text, labels |
--fg-faint | #6C7597 | #98A0B8 | faint text, tertiary |
--rule | rgba(242,240,234,.17) | rgba(32,40,69,.14) | hairlines, outlines, dividers |
--rule-strong | rgba(242,240,234,.32) | rgba(32,40,69,.30) | emphasised borders |
--tile | #F2F0EA | #202845 | resting letter tile face |
--tile-ink | #202845 | #F2F0EA | letter on a resting tile |
--die-edge | #7E828F | #9496A0 | tile edge detail |
--select | #202845 | #F2F0EA | selected (lit) tile fill |
--select-ink | #F2F0EA | #202845 | letter on a selected tile |
--select-stroke | rgba(242,240,234,.5) | rgba(32,40,69,.5) | selected tile border |
--lit | #3D445C | #D5D4D3 | lit surface (non-tile) |
--lit-ink | #F2F0EA | #202845 | ink on a lit surface |
--lit-edge | #2D354F | #E5E3E0 | lit edge detail |
--trace | #F2F0EA | #202845 | the connecting trace line |
--earned | #7BE0A3 | #276A43 | positive — valid word / a win |
--danger | #F2B8BC | #8E2430 | negative — invalid / error |
--focus | #7E9BE0 | #2F58D0 | focus ring |
--earned-wash | rgba(123,224,163,.12) | rgba(39,106,67,.10) | green tint background |
--danger-wash | rgba(242,184,188,.14) | rgba(142,36,48,.10) | rose tint background |
--earned-card-bg | rgba(123,224,163,.06) | #E6EEE3 | earned card fill |
--earned-card-border | rgba(123,224,163,.45) | #9FC2A9 | earned card border |
--mark-dot | #9698A0 | #686D7D | brand-mark lattice dots |
--shadow | rgba(10,14,34,.38) | rgba(32,40,69,.16) | drop shadow |
--shadow-lift | rgba(10,14,34,.28) | rgba(32,40,69,.20) | button lift shadow |
--scrim | rgba(10,14,34,.72) | rgba(32,40,69,.55) | modal scrim |
Generated from css/tokens.css and the spec's colour table at 3.25.7.
Contrast
| Token | Dark · ground | Dark · card | Light · ground | Light · card |
|---|---|---|---|---|
--fgbody text | 12.70AAA | 10.14AAA | 12.70AAA | 13.75AAA |
--fg-softsecondary text | 6.36AA | 5.07AA | 4.78AA | 5.17AA |
--fg-fainttertiary | 3.19UI only | 2.55decorative | 2.29decorative | 2.48decorative |
--earnedearned (green) | 8.98AAA | 7.17AAA | 5.71AA | 6.18AA |
--dangerrefused (rose) | 8.52AAA | 6.80AA | 7.54AAA | 8.16AAA |
--focusfocus ring | 5.27AA | 4.21UI only | 5.38AA | 5.82AA |
WCAG contrast, computed from the tokens on every build. AA needs 4.5:1 for text, 3:1 for large text and UI.
Body text, secondary text and both semantic hues pass AA on the ground and on cards in both modes. --fg-faint doesn't, which is why it's for non-text only (see Known issues).
The two semantic hues
Green (--earned) and rose (--danger) are the only colours that carry meaning, and each means exactly one thing.
- Green means earned. A valid word, a win, a game you've finished today. It isn't for recaps, badges or progress, which are carried by weight and ink.
- Green is never a selection (3.21.1). A word shown or traced on a review board lights in the pressed-tile state (
--select,--select-stroke,--select-ink), and its pill flips the same way. Showing a word earns nothing. - Rose means refused. Not a word, a form error, the clock running out.
Legacy names
Older class names still work because aliases point at the canonical roles in one place: --tray → --ground, --tray-raise → --raise, --tray-line → --rule, --paper → --fg, --paper-dim → --fg-soft, --gold → --fg, --ok-green → --earned, --bad-fill → --danger, and a dozen more. They're deprecated: never use them in anything new. Moving the existing uses over is planned (see System health).
Why it's built this way
Known issues and gaps
From the 3.23.6 audit:
--fg-faintreads 3.19:1 on dark and 2.29:1 on light, below the 4.5:1 text floor, yet three rules use it for text. Fix: those become--fg-soft.- 21 colour literals remain in component CSS, including a modal overlay that doesn't change in light mode and a pale blue (
rgba(184,204,239,…)) in 8 rules that isn't in the palette. - 47% of colour references still use legacy names.
- 6 focus rules use
--goldand 2 use--focus. They should all use--focus.