Skip to content

Accessibility

Static Bloom guarantees what a palette can guarantee: every color meant for text is readable on the backgrounds it is meant for, in every flavor. The rest (focus, keyboard, motion, layout) is built into the components and is yours to keep when you compose them.

pnpm check:contrast runs the palette gate and exits with an error on any failure, so a color that drops below its threshold cannot be released. The table recomputes the WCAG 2.x rules from the published palette when this site is built. Each cell shows the weakest pair the rule covers.

WCAG contrast gate: the weakest pair of every rule in every flavor
RuleMinimumDuskMidnightBlackoutDaybreak
text on crust, mantle, base, surface07.0:111.31 passtext on surface013.76 passtext on surface016.32 passtext on surface012.37 passtext on crust
text on surface1, surface24.5:18.27 passtext on surface210.08 passtext on surface212.96 passtext on surface210.24 passtext on surface2
subtext1 on crust, mantle, base, surface04.5:18.40 passsubtext1 on surface09.73 passsubtext1 on surface010.87 passsubtext1 on surface07.50 passsubtext1 on crust
subtext0 on crust, mantle, base4.5:17.06 passsubtext0 on base7.36 passsubtext0 on base7.59 passsubtext0 on base5.32 passsubtext0 on crust
overlay2 on base3.0:14.69 passoverlay2 on base4.61 passoverlay2 on base4.62 passoverlay2 on base4.67 passoverlay2 on base
overlay1 on base, mantle3.0:13.24 passoverlay1 on base3.17 passoverlay1 on base3.10 passoverlay1 on base3.10 passoverlay1 on mantle
overlay0 on base1.4:12.16 passoverlay0 on base2.10 passoverlay0 on base1.83 passoverlay0 on base1.74 passoverlay0 on base
each accent on crust, mantle, base, surface04.5:14.67 passultraviolet on surface05.75 passultraviolet on surface06.64 passultraviolet on surface04.50 passcoolant on crust
on-accent on each accent4.5:16.33 passcrust on ultraviolet7.07 passcrust on ultraviolet7.40 passcrust on ultraviolet5.40 passbase on coolant
accents closest pair, ΔEOK0.060.061 passfrost / chrome0.061 passfrost / chrome0.061 passfrost / chrome0.064 passember / sodium

The gate also checks identity: every dark accent must stay at least 0.07 ΔEOK from seven Cyberpunk 2077 UI reference colors. It prints APCA Lc values for text roles as information.

What follows from the table:

  • text is 7:1 (AAA) on every background and 4.5:1 on raised fills; subtext1 and subtext0 are body-safe on the page backgrounds.
  • Every accent is AA on crust, mantle, base and surface0, so colored text, links and outline badges stay readable.
  • Soft badges and buttons sit on a 16% tint of their own color, so their text mixes the intent with text per flavor (--sb-soft-ink: 100% in Blackout, 90% in Midnight, 75% in Dusk and Daybreak). Soft, outline and ghost buttons use the same ink on hover, and the soft hover keeps its tint. That keeps them AA on crust, mantle, base and surface0. The Vue tests measure it in every flavor on those four surfaces: soft badges in every intent and accent and soft buttons in every intent at rest, and soft, outline and ghost buttons in every intent on hover.
  • on-accent text is AA on every solid accent fill, which covers solid buttons and badges.
  • overlay1 (line-strong) meets the 3:1 non-text minimum for input borders and control edges; overlay0 (line) is decorative only.

Every focusable element gets a visible ring on keyboard focus (:focus-visible): a 2px outline in --sb-ring (the primary intent) at a 2px offset, plus a small glow. Tab through these controls to see it:

Link

Tokens: --sb-ring, --sb-ring-width (2px), --sb-ring-offset (2px) and --sb-ring-offset-color (the canvas). Do not remove the outline; restyle it through the tokens instead.

  • CSS components build on native elements (<button>, <a>, <details>, <dialog>, the Popover API), so keyboard behavior comes from the browser.
  • Vue components wrap Reka UI primitives: arrow-key roving focus in toolbars and toggle groups, typeahead in lists, Escape to close the topmost overlay, focus trapped inside dialogs and returned to the trigger on close, and the ARIA roles and states each pattern needs.
  • Icon-only buttons need an accessible name: give them aria-label, since title tooltips are not allowed in game.

Decorative animation stops under prefers-reduced-motion: reduce: durations collapse, loops run once, and the Tailwind animate-* utilities switch off. See Effects & motion.

Intent colors always travel with words or icons: alerts have titles, badges have text, stat bars show their value, and form errors are written out under the field with aria-invalid set. Keep that pairing when you compose your own components.

Button heights are 1.75rem (xs), 2rem (sm), 2.5rem (md, the default) and 3rem (lg), above the 24px WCAG 2.2 minimum. Inputs are 2.5rem tall. On touch layouts, prefer md or larger.

OPEN//77 pages are Chromium pages drawn over the game, which adds constraints:

  • No native popups. <select>, date, time and color inputs, <datalist> and title tooltips open browser-drawn popups that do not render reliably over the game. The Vue components draw every list, popover and tooltip in the page instead.
  • Legibility over the game. The scene behind a page can be anything, so never rely on it being dark: use Blackout, put text on glass or solid panels, and keep it large enough. HUD legibility has the measured contrast per flavor.
  • Focus belongs to the game. A page that takes keyboard focus stops game input. Release it on every exit path, and give each dialog a visible close button, because the platform handles Escape itself unless the page asks to consume it.

In-game UIs has the details and the client API.