# How the WCAG Contrast Ratio Check Works in Design.md

> Understand how the WCAG contrast ratio check works. Learn the four pipeline steps to ensure accessibility compliance and avoid design violations.

- Repository: [Google Labs Code/design.md](https://github.com/google-labs-code/design.md)
- Tags: how-to-guide
- Published: 2026-06-26

---

**The WCAG contrast ratio check follows a four-step pipeline: parsing design tokens into resolved colors with pre-computed luminance, calculating the WCAG 2.1 contrast ratio using `(L1 + 0.05) / (L2 + 0.05)`, comparing results against the 4.5:1 AA minimum, and emitting detailed lint warnings for violations.**

The [`google-labs-code/design.md`](https://github.com/google-labs-code/design.md/blob/main/google-labs-code/design.md) repository implements accessibility validation as a core CLI feature. The contrast checker operates on resolved color tokens to ensure text remains readable against background colors, automatically handling token indirection and color aliases.

## Understanding the WCAG Contrast Ratio Algorithm

The implementation follows the **WCAG 2.1 contrast ratio formula** exactly. Each color is converted to a linearized **luminance** value ranging from 0 (black) to 1 (white). The algorithm divides the lighter luminance plus 0.05 by the darker luminance plus 0.05 to produce a ratio where 1:1 represents no contrast and 21:1 represents maximum contrast (black on white).

## Step-by-Step Implementation

### Design System Parsing

The linter first parses the [`DESIGN.md`](https://github.com/google-labs-code/design.md/blob/main/DESIGN.md) file into a `DesignSystemState`. During this phase, the CLI resolves component properties like `backgroundColor` and `textColor` into **`ResolvedColor`** objects. These objects contain pre-computed luminance values, enabling efficient contrast calculations without repetitive color space conversions.

### Contrast Calculation

The core mathematics live in [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts). The `contrastRatio` function accepts two `ResolvedColor` objects and returns the WCAG-defined ratio:

```ts
// packages/cli/src/linter/model/handler.ts#L400-L406
export function contrastRatio(a: ResolvedColor, b: ResolvedColor): number {
  const L1 = Math.max(a.luminance, b.luminance);
  const L2 = Math.min(a.luminance, b.luminance);
  return (L1 + 0.05) / (L2 + 0.05);
}

```

This implementation strictly follows the WCAG specification by selecting the maximum luminance as the numerator and minimum as the denominator, ensuring the ratio always represents the relative brightness difference regardless of color order.

### Rule Execution

The `contrastCheck` rule in [`packages/cli/src/linter/rules/contrast-ratio.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/rules/contrast-ratio.ts) orchestrates the validation. It iterates through every component in the design system, extracts resolved background and text colors, and invokes `contrastRatio`:

```ts
// packages/cli/src/linter/rules/contrast-ratio.ts#L19-L44
const WCAG_AA_MINIMUM = 4.5;

export function contrastCheck(state: DesignSystemState): RuleFinding[] {
  const findings: RuleFinding[] = [];
  for (const [compName, comp] of state.components) {
    const bgValue = comp.properties.get('backgroundColor');
    const textValue = comp.properties.get('textColor');
    if (!bgValue || !textValue) continue;

    const bgColor = resolveToColor(bgValue);
    const textColor = resolveToColor(textValue);
    if (!bgColor || !textColor) continue;

    const ratio = contrastRatio(bgColor, textColor);
    if (ratio < WCAG_AA_MINIMUM) {
      findings.push({
        path: `components.${compName}`,
        message: `textColor (${textColor.hex}) on backgroundColor (${bgColor.hex}) has contrast ratio ${ratio.toFixed(2)}:1, below WCAG AA minimum of ${WCAG_AA_MINIMUM}:1.`,
      });
    }
  }
  return findings;
}

```

The rule specifically checks against the **WCAG AA minimum of 4.5:1** for normal text, creating a `RuleFinding` warning when colors fall below this threshold.

### Reporting and CLI Output

When the linter detects insufficient contrast, it generates a structured warning containing the component path, specific hex values, and the computed ratio. The CLI's `lint` command formats these findings for display, showing exactly which color combination failed and by what margin.

## Practical Usage Examples

### Running the CLI Linter

Execute the contrast check against any [`DESIGN.md`](https://github.com/google-labs-code/design.md/blob/main/DESIGN.md) file using the CLI:

```bash

# Lint a DESIGN.md file and view contrast warnings

design-md lint path/to/DESIGN.md --format=text

```

Typical output identifies specific violations:

```json
{
  "findings": [
    {
      "path": "components.Button",
      "message": "textColor (#777777) on backgroundColor (#FFFFFF) has contrast ratio 3.98:1, below WCAG AA minimum of 4.5:1."
    }
  ],
  "summary": { "warnings": 1, "errors": 0 }
}

```

### Using the Contrast Function Programmatically

Access the calculation logic directly for custom tooling:

```ts
import { contrastRatio } from '@design-md/cli';
import { resolveColor } from '@design-md/cli/model';

const bg = resolveColor('#ffffff'); // luminance ≈ 1.0
const txt = resolveColor('#777777'); // luminance ≈ 0.42

const ratio = contrastRatio(bg, txt);
console.log(`Contrast ratio: ${ratio.toFixed(2)}:1`); // → 3.98:1

```

### Creating Custom Rules

Extend the linter with custom contrast thresholds by reusing the core function:

```ts
import { contrastRatio } from '@design-md/cli/model/handler';
import type { DesignSystemState, ResolvedColor } from '@design-md/cli/model/spec';

export function strictContrastRule(state: DesignSystemState) {
  const findings = [];
  // Iterate components...
  const ratio = contrastRatio(colorA, colorB);
  if (ratio < 5) {
    findings.push({ 
      path: 'components.foo', 
      message: `Low contrast: ${ratio.toFixed(2)}:1` 
    });
  }
  return findings;
}

```

## Key Source Files and Architecture

The contrast checking system spans four primary modules:

- **[`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts)** – Contains the `contrastRatio` function and color resolution logic that computes luminance values for resolved tokens.

- **[`packages/cli/src/linter/rules/contrast-ratio.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/rules/contrast-ratio.ts)** – Implements the `contrastCheck` rule that traverses components and emits accessibility warnings.

- **[`packages/cli/src/commands/lint.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/commands/lint.ts)** – CLI entry point that executes the linter and formats output for terminal display.

- **[`packages/cli/src/linter/index.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/index.ts)** – Rule registry where `contrastCheck` is registered alongside other lint rules.

The architecture separates color resolution from rule logic, allowing the contrast calculation to work on fully resolved colors regardless of token indirection depth or alias complexity.

## Summary

- The WCAG contrast ratio check calculates relative luminance using `(L1 + 0.05) / (L2 + 0.05)` as defined in WCAG 2.1.
- The `contrastRatio` function in [`handler.ts`](https://github.com/google-labs-code/design.md/blob/main/handler.ts) operates on `ResolvedColor` objects containing pre-computed luminance values.
- The `contrastCheck` rule enforces a **4.5:1 minimum ratio** for WCAG AA compliance across all components with `backgroundColor` and `textColor` properties.
- Violations generate detailed warnings showing exact hex values and calculated ratios.
- The modular architecture allows programmatic reuse of `contrastRatio` for custom linting rules or external tooling.

## Frequently Asked Questions

### What WCAG conformance level does the contrast checker enforce?

The `contrastCheck` rule enforces **WCAG 2.1 Level AA** standards, which require a minimum contrast ratio of **4.5:1** for normal text. This threshold is hardcoded as `WCAG_AA_MINIMUM = 4.5` in [`packages/cli/src/linter/rules/contrast-ratio.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/rules/contrast-ratio.ts).

### How does the checker handle design tokens and color aliases?

The checker operates on **resolved colors** rather than raw token strings. During the parsing phase, the CLI resolves all token references, aliases, and nested color values into `ResolvedColor` objects containing computed luminance values. This ensures the contrast calculation evaluates the final color values regardless of token indirection depth.

### Can I use the contrast calculation in my own scripts outside the CLI?

Yes. The `contrastRatio` function is exported from [`packages/cli/src/linter/model/handler.ts`](https://github.com/google-labs-code/design.md/blob/main/packages/cli/src/linter/model/handler.ts) and can be imported programmatically. You must first resolve color values to `ResolvedColor` objects (which include luminance data) before passing them to the function.

### Why does the contrast ratio formula add 0.05 to luminance values?

The **0.05 offset** prevents division by zero when calculating ratios involving pure black (0 luminance) and accounts for ambient light effects in the WCAG 2.1 specification. This constant ensures the ratio between two identical colors equals 1:1 and maximum contrast (black vs white) equals 21:1.