How the contrast-ratio Linting Rule Works in Design.md

The contrast-ratio linting rule enforces WCAG AA accessibility standards by calculating the luminance contrast between backgroundColor and textColor properties in Design.md components, flagging any pair below the 4.5:1 threshold.

The contrast-ratio linting rule is a critical accessibility feature within the Design.md CLI linter. As part of the google-labs-code/design.md repository, this rule ensures that UI components meet WCAG AA standards by verifying that text remains readable against its background. By analyzing fully resolved color values in design system components, the rule automatically surfaces warnings when color combinations fail to provide sufficient contrast.

Rule Architecture and Implementation

Core Implementation File

The rule logic resides in packages/cli/src/linter/linter/rules/contrast-ratio.ts, where it exports a RuleDescriptor with name: 'contrast-ratio' and severity: 'warning' (lines 54-58). This descriptor registers the contrastCheck function as the entry point for linting operations. The rule specifically targets components that define both backgroundColor and textColor properties, skipping any component missing either attribute.

Registration and Discovery

The packages/cli/src/linter/linter/rules/index.ts file re-exports the rule, making it discoverable by the linter runtime. This registration pattern allows the CLI to automatically include the contrast check when linting Design.md files without requiring manual configuration. The rule is then exercised by the test suite in packages/cli/src/linter/linter/rules/contrast-ratio.test.ts, which verifies correct detection of low-contrast color pairs.

How the Contrast Calculation Works

Component Iteration and Validation

The contrastCheck function iterates over every component in state.components (lines 27-30). For each component, it looks for the two specific properties: backgroundColor and textColor. The helper function resolveToColor (lines 47-51) validates that the resolved values are actual color objects (type === 'color'), causing the rule to skip components with non-color values or undefined properties.

WCAG Luminance Formula Implementation

Once both colors are resolved to ResolvedColor objects, the rule calls contrastRatio(bgColor, textColor) at line 36. This function lives in packages/cli/src/linter/model/handler.ts (lines 400-406) and implements the standard WCAG luminance algorithm:

const L1 = Math.max(a.luminance, b.luminance);
const L2 = Math.min(a.luminance, b.luminance);
return (L1 + 0.05) / (L2 + 0.05);

The formula divides the lighter color's relative luminance (plus 0.05) by the darker color's relative luminance (plus 0.05) to produce a contrast ratio.

Threshold Validation

The computed ratio compares against the constant WCAG_AA_MINIMUM = 4.5 (line 19). If the result falls below this threshold, the rule generates a RuleFinding (lines 37-42) containing a human-readable message that specifies the exact ratio calculated and identifies the offending color values. This finding includes the component path for precise error tracking.

Token Resolution Pipeline

Before the contrast check executes, the ModelHandler fully resolves all token references through the design system pipeline. This guarantees that the rule evaluates concrete ResolvedColor objects containing RGB representations and pre-computed WCAG luminance values rather than raw reference strings. For example, a textColor defined as {colors.primary} transforms into its actual hex or RGB value before the contrastCheck function begins its evaluation, ensuring accurate mathematical comparison regardless of how colors are referenced in the source YAML.

Practical Code Examples

Passing Configuration

The following component satisfies the WCAG AA standard:

components:
  Button:
    backgroundColor: "#ffffff"
    textColor: "#000000"

Running the CLI linter produces:

$ design-md lint my-design.yaml
✔ contrast‑ratio: all components satisfy WCAG AA

Failing Configuration

This combination triggers a warning due to insufficient contrast:

components:
  Card:
    backgroundColor: "#fafafa"
    textColor: "#b0b0b0"

Linter output:


⚠ contrast‑ratio:
  components.Card – textColor (#b0b0b0) on backgroundColor (#fafafa) has contrast ratio 2.34:1, below WCAG AA minimum of 4.5:1.

Token-Based Colors

Token references are resolved before evaluation:

colors:
  primary: "#0055aa"
  onPrimary: "#ffffff"

components:
  Header:
    backgroundColor: "{colors.primary}"
    textColor: "{colors.onPrimary}"

The ModelHandler resolves these references to concrete colors, after which the contrast-ratio rule evaluates them as it would with static hex values.

Summary

  • The rule is implemented in packages/cli/src/linter/linter/rules/contrast-ratio.ts as a RuleDescriptor with severity: 'warning'
  • It requires both backgroundColor and textColor properties to perform the check, skipping components with missing or non-color values
  • The WCAG AA minimum threshold is strictly enforced at 4.5:1 via the WCAG_AA_MINIMUM constant
  • Color resolution happens through ModelHandler before linting, supporting complex token references like {colors.primary}
  • Failed checks generate detailed RuleFinding objects showing the exact contrast ratio and offending color values

Frequently Asked Questions

What is the minimum contrast ratio required by the Design.md linting rule?

The rule enforces a minimum contrast ratio of 4.5:1, defined by the constant WCAG_AA_MINIMUM at line 19 of contrast-ratio.ts. This value aligns with WCAG AA standards for normal text, ensuring sufficient readability for users with vision impairments while maintaining flexibility for large-scale text exceptions.

How does the contrast-ratio rule handle token references in Design.md?

The ModelHandler resolves all token references before the contrastCheck function executes. References like {colors.primary} are converted into concrete ResolvedColor objects containing RGB values and pre-calculated luminance data, allowing the rule to evaluate actual colors rather than reference strings.

Why does the contrast-ratio rule use warning severity instead of error?

According to the RuleDescriptor in contrast-ratio.ts (lines 54-58), the rule uses severity: 'warning' to allow developers to make intentional design decisions while still being notified of accessibility concerns. This approach acknowledges that design systems may occasionally require exceptions for branding or aesthetic reasons that override strict accessibility compliance.

Where is the WCAG luminance calculation implemented in the source code?

The actual luminance calculation resides in packages/cli/src/linter/model/handler.ts (lines 400-406) within the contrastRatio function. This utility calculates the relative luminance of two colors using the WCAG formula (L1 + 0.05) / (L2 + 0.05), where L1 represents the lighter color's luminance and L2 represents the darker color's luminance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →