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.tsas aRuleDescriptorwithseverity: 'warning' - It requires both
backgroundColorandtextColorproperties 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_MINIMUMconstant - Color resolution happens through
ModelHandlerbefore linting, supporting complex token references like{colors.primary} - Failed checks generate detailed
RuleFindingobjects 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →