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.

LevelRatioApplies to
AA4.5:1Normal text (< 18pt / < 14pt bold)
AA Large3:1Large text (≥ 18pt or ≥ 14pt bold) and UI components
AAA7:1Enhanced — 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.

Aa
--text-primaryon--background (dark)
17.8:1AAA
Aa
--text-secondaryon--background (dark)
7.5:1AAA
Aa
--text-tertiaryon--background (dark)
4.5:1AA
Aa
--nav-active-color (dark)on--background (dark)
5.4:1AA
Aa
--text-primaryon--surface (light)
16.5:1AAA
Aa
--text-secondaryon--surface (light)
8.5:1AAA
Aa
--text-tertiaryon--surface (light)
5.5:1AA

Rules

✓ Do

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

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.

✓ Do

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.

5.4:1 ✓
4.6:1 ✓
✗ Don't

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.

Error
Success

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().

accessibleForeground.ts
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";
}
Color+Accessible.swift (Warren)
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

#3B82F6Blue 500→ black
#10B981Emerald 500→ black
#F59E0BAmber 500→ black
#EF4444Red 500→ black
#F3F4F6Gray 100→ black
#1F2937Gray 800→ white
#FBBF24Amber 400→ black
#7C3AEDViolet 600→ white
#FFFFFFWhite→ black
#000000Black→ white
Threshold

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.

Input format

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.

Usage contexts

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.