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.
Contrast gate
Section titled “Contrast gate”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.
| Rule | Minimum | Dusk | Midnight | Blackout | Daybreak |
|---|---|---|---|---|---|
text on crust, mantle, base, surface0 | 7.0:1 | 11.31 passtext on surface0 | 13.76 passtext on surface0 | 16.32 passtext on surface0 | 12.37 passtext on crust |
text on surface1, surface2 | 4.5:1 | 8.27 passtext on surface2 | 10.08 passtext on surface2 | 12.96 passtext on surface2 | 10.24 passtext on surface2 |
subtext1 on crust, mantle, base, surface0 | 4.5:1 | 8.40 passsubtext1 on surface0 | 9.73 passsubtext1 on surface0 | 10.87 passsubtext1 on surface0 | 7.50 passsubtext1 on crust |
subtext0 on crust, mantle, base | 4.5:1 | 7.06 passsubtext0 on base | 7.36 passsubtext0 on base | 7.59 passsubtext0 on base | 5.32 passsubtext0 on crust |
overlay2 on base | 3.0:1 | 4.69 passoverlay2 on base | 4.61 passoverlay2 on base | 4.62 passoverlay2 on base | 4.67 passoverlay2 on base |
overlay1 on base, mantle | 3.0:1 | 3.24 passoverlay1 on base | 3.17 passoverlay1 on base | 3.10 passoverlay1 on base | 3.10 passoverlay1 on mantle |
overlay0 on base | 1.4:1 | 2.16 passoverlay0 on base | 2.10 passoverlay0 on base | 1.83 passoverlay0 on base | 1.74 passoverlay0 on base |
each accent on crust, mantle, base, surface0 | 4.5:1 | 4.67 passultraviolet on surface0 | 5.75 passultraviolet on surface0 | 6.64 passultraviolet on surface0 | 4.50 passcoolant on crust |
on-accent on each accent | 4.5:1 | 6.33 passcrust on ultraviolet | 7.07 passcrust on ultraviolet | 7.40 passcrust on ultraviolet | 5.40 passbase on coolant |
accents closest pair, ΔEOK | 0.06 | 0.061 passfrost / chrome | 0.061 passfrost / chrome | 0.061 passfrost / chrome | 0.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:
textis 7:1 (AAA) on every background and 4.5:1 on raised fills;subtext1andsubtext0are body-safe on the page backgrounds.- Every accent is AA on
crust,mantle,baseandsurface0, 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
textper 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 oncrust,mantle,baseandsurface0. 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-accenttext 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:
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.
Keyboard
Section titled “Keyboard”- 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, sincetitletooltips are not allowed in game.
Motion
Section titled “Motion”Decorative animation stops under prefers-reduced-motion: reduce: durations collapse, loops run once, and the Tailwind animate-* utilities switch off. See Effects & motion.
Color is never the only signal
Section titled “Color is never the only signal”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.
Targets
Section titled “Targets”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.
In-game pages
Section titled “In-game pages”OPEN//77 pages are Chromium pages drawn over the game, which adds constraints:
- No native popups.
<select>, date, time and color inputs,<datalist>andtitletooltips 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.