skynet

Accessibility

Checked by the build, not by eye

Every colour token that is ever text or a control boundary declares what it must be legible against and at what ratio. The token build measures each pair and fails if one falls short. Text targets 4.5:1 and controls and focus rings 3:1 (WCAG AA), in both themes. Below: what the site shipped with, and what it measures now.

Before

Five pairs the old palette failed

Contrast of the previous palette
PairRatioNeededResult
light accent text (#00aa2e) on the page2.84:14.5:1fail
white on the light accent button3.09:14.5:1fail
light pending (#ff8800) on the page2.20:14.5:1fail
dark field border (#4a4a4a) on a card1.96:13:1fail
light field border (#d0d0d0) on white1.54:13:1fail

The fix was two new primitives, a darker green and a darker amber for the light theme, and a lighter grey for control borders in both. Dark text colours already passed. The dark theme's look did not change.

After

40 declared pairs, all passing

dark 24 pairs

Contrast pairs in the dark theme
TokenAgainstColoursRatioNeededGrade
color.text.primarysurface.pageAa #e0e0e0 / #0d0d0d14.72:17:1 textAAA
color.text.primarysurface.raisedAa #e0e0e0 / #1a1a1a13.18:17:1 textAAA
color.text.secondarysurface.pageAa #c0c0c0 / #0d0d0d10.68:14.5:1 textAAA
color.text.secondarysurface.raisedAa #c0c0c0 / #1a1a1a9.57:14.5:1 textAAA
color.text.mutedsurface.pageAa #a0a0a0 / #0d0d0d7.43:14.5:1 textAAA
color.text.mutedsurface.raisedAa #a0a0a0 / #1a1a1a6.66:14.5:1 textAA
color.text.mutedsurface.sunkenAa #a0a0a0 / #2525255.86:14.5:1 textAA
color.text.accentsurface.pageAa #16ff9a / #0d0d0d14.63:14.5:1 textAAA
color.text.accentsurface.raisedAa #16ff9a / #1a1a1a13.10:14.5:1 textAAA
color.text.linksurface.pageAa #16ff9a / #0d0d0d14.63:14.5:1 textAAA
color.text.linksurface.raisedAa #16ff9a / #1a1a1a13.10:14.5:1 textAAA
color.text.on-accentaccent.fillAa #000000 / #16ff9a15.80:14.5:1 textAAA
color.accent.edgesurface.pageAa #16ff9a / #0d0d0d14.63:13:1 uiAAA
color.accent.edgesurface.raisedAa #16ff9a / #1a1a1a13.10:13:1 uiAAA
color.border.controlsurface.pageAa #767676 / #0d0d0d4.28:13:1 non-textAA large / non-text only
color.border.controlsurface.raisedAa #767676 / #1a1a1a3.83:13:1 non-textAA large / non-text only
color.border.focussurface.pageAa #16ff9a / #0d0d0d14.63:13:1 non-textAAA
color.border.focussurface.raisedAa #16ff9a / #1a1a1a13.10:13:1 non-textAAA
color.status.winsurface.pageAa #16ff9a / #0d0d0d14.63:14.5:1 textAAA
color.status.winsurface.raisedAa #16ff9a / #1a1a1a13.10:14.5:1 textAAA
color.status.losssurface.pageAa #ff4444 / #0d0d0d5.70:14.5:1 textAA
color.status.losssurface.raisedAa #ff4444 / #1a1a1a5.11:14.5:1 textAA
color.status.pendingsurface.pageAa #ffa500 / #0d0d0d9.84:14.5:1 textAAA
color.status.pendingsurface.raisedAa #ffa500 / #1a1a1a8.81:14.5:1 textAAA

light 16 pairs

Contrast pairs in the light theme
TokenAgainstColoursRatioNeededGrade
color.text.primarysurface.pageAa #1a1a1a / #f5f5f515.96:17:1 textAAA
color.text.primarysurface.raisedAa #1a1a1a / #ffffff17.40:17:1 textAAA
color.text.secondarysurface.pageAa #333333 / #f5f5f511.59:14.5:1 textAAA
color.text.secondarysurface.raisedAa #333333 / #ffffff12.63:14.5:1 textAAA
color.text.mutedsurface.pageAa #666666 / #f5f5f55.27:14.5:1 textAA
color.text.mutedsurface.raisedAa #666666 / #ffffff5.74:14.5:1 textAA
color.text.mutedsurface.sunkenAa #666666 / #e8e8e84.69:14.5:1 textAA
color.text.on-accentaccent.fillAa #000000 / #16ff9a15.80:14.5:1 textAAA
color.accent.edgesurface.pageAa #007a20 / #f5f5f55.06:13:1 uiAA
color.accent.edgesurface.raisedAa #007a20 / #ffffff5.52:13:1 uiAA
color.border.controlsurface.pageAa #8a8a8a / #f5f5f53.17:13:1 non-textAA large / non-text only
color.border.controlsurface.raisedAa #8a8a8a / #ffffff3.45:13:1 non-textAA large / non-text only
color.status.losssurface.pageAa #cc0000 / #f5f5f55.40:14.5:1 textAA
color.status.losssurface.raisedAa #cc0000 / #ffffff5.89:14.5:1 textAA
color.status.pendingsurface.pageAa #a85a00 / #f5f5f54.67:14.5:1 textAA
color.status.pendingsurface.raisedAa #a85a00 / #ffffff5.09:14.5:1 textAA
Beyond colour

What the primitives guarantee

  • Never colour alone. Outcomes are a glyph plus a word. Selection has a check mark. Edge signs are a glyph, a colour and hidden text. Errors are text.
  • Keyboard. One focus ring for everything, visible on keyboard focus only. Segmented controls are radiogroups with arrow keys. Wide tables scroll inside a focusable region. Disabled buttons stay in the tab order and say why.
  • Labels. A control cannot exist without a Field; clicking the label focuses it; hints and errors are read with it.
  • Targets. Controls are 40px tall by default and never under 24px (WCAG 2.5.8).
  • Text size. Anything typed into is 16px, so iOS does not zoom on focus.
  • Motion. Durations are tokens, so prefers-reduced-motion collapses all of them in one rule; the same query removes the two movements the system has (press scale, tooltip rise) and keeps the fades.
  • Long strings. Addresses truncate in the middle, keep the full value for assistive tech, and break anywhere rather than overflow when shown whole.