Contrast
Every text and interactive element in Sitka is designed to meet WCAG 2.1 AA as a minimum. This page documents approved token pairings and the rules for maintaining accessible contrast.
WCAG Levels
Sitka targets AA for all text and interactive elements. AAA is met where possible for primary reading text.
| Level | Ratio | Applies to |
|---|---|---|
| AA | 4.5:1 | Normal text (< 18pt / < 14pt bold) |
| AA Large | 3:1 | Large text (≥ 18pt or ≥ 14pt bold) and UI components |
| AAA | 7:1 | Enhanced — primary reading text target |
Approved Token Pairings
These are the only validated foreground/background combinations. Do not introduce new pairings without running them through a contrast checker.
--text-primaryon--background (dark)--text-secondaryon--background (dark)--text-tertiaryon--background (dark)--nav-active-color (dark)on--background (dark)--text-primaryon--surface (light)--text-secondaryon--surface (light)--text-tertiaryon--surface (light)Rules
Use semantic text tokens
Place --text-primary on --background or --surface. These pairings are pre-validated to meet WCAG AA at all sizes.
Account settings
Manage your profile and preferences.
Don't use tertiary text for body copy
--text-tertiary is only guaranteed to pass at 18pt+ or bold weights. Never use it for continuous reading text.
Account settings
Manage your profile and preferences.
Test both light and dark
A colour pair that passes in dark mode may fail in light mode. Always validate the token pairing against both theme values.
Don't rely on colour alone
Always pair colour with a secondary indicator — icon, label, pattern, or position — so the meaning survives greyscale or colour-blind vision.
accessibleForeground() utility
When rendering text on a dynamic background — colour picker swatches, user-assigned tags, status badges — you cannot hardcode a foreground colour. Use accessibleForeground()to compute whether black or white text produces the higher contrast ratio against any background hex. This is the TypeScript port of Warren's Swift sfAccessibleForeground() function, shipped in the library as import { accessibleForeground } from "@sitka/react" alongside relativeLuminance() and contrastRatio().
function relativeLuminance(hex: string): number {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const toLinear = (c: number) =>
c <= 0.04045
? c / 12.92
: Math.pow((c + 0.055) / 1.055, 2.4);
return (
0.2126 * toLinear(r) +
0.7152 * toLinear(g) +
0.0722 * toLinear(b)
);
}
export function accessibleForeground(
background: string
): "#000000" | "#ffffff" {
return relativeLuminance(background) > 0.179
? "#000000"
: "#ffffff";
}extension Color {
/// Returns .black or .white for legible
/// text on any background colour.
func accessibleForeground() -> Color {
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
UIColor(self).getRed(&r, green: &g,
blue: &b, alpha: nil)
func linearise(_ c: CGFloat) -> CGFloat {
c <= 0.04045
? c / 12.92
: pow((c + 0.055) / 1.055, 2.4)
}
let lum = 0.2126 * linearise(r)
+ 0.7152 * linearise(g)
+ 0.0722 * linearise(b)
return lum > 0.179 ? .black : .white
}
}Example outputs
The luminance threshold of 0.179 corresponds to a ~4.5:1 contrast ratio with both black and white at the crossover point. It is not exactly 0.5 because the luminance scale is perceptually non-linear.
The function expects a 6-character hex string with a leading # (e.g. #3B82F6). Extend it to accept rgb() or hsl() strings if your palette uses those formats.
Use for tag chips, colour picker overlays, user-assigned badge colours, status indicators, and any surface where the background is not known at design time.
Testing Tools
Polypane
Real-time contrast overlays in the browser. Supports the full APCA model.
Figma Stark
Contrast checker and colour-blind simulation directly in design files.
axe DevTools
Automated accessibility audits in CI and in the browser extension.