Olyx uses an HCT-inspired color system built entirely in CSS custom properties. You set a single brand color — the rest is generated automatically with WCAG AA accessible contrast baked in.

No JavaScript. No build step. Just CSS variables.

HCT Color Space

The system is based on Material Design 3's HCT color space, which defines colors using three dimensions:

The key insight: tone directly maps to perceived lightness, which makes accessible contrast pairings predictable. Two colors with a tone difference ≥ 40 reliably produce ≥ 3:1 contrast. That's the entire trick.

Our implementation approximates HCT using HSL with clamped saturation and computed lightness values — close enough for web rendering, no color science library required.

Color Tokens

All color tokens in Olyx follow the HCT-based color role pattern.

Primary

Secondary

Tertiary

Error

Success

Info

Warning

Surface

Outline

How It Works in Olyx

1. Set Your Brand Color

Two variables control everything:

:root {
  --brand-hue: 250;        /* 0–360, your brand's hue */
  --brand-saturation: 60%; /* how vivid */
}

2. Key Colors Are Derived

From your brand, five key color groups are generated — same concept as Material's key colors:

3. Tones Are Assigned to Roles

Each key color gets mapped to color roles using specific tone values — light/dark modes simply swap which tones go where:

The same pattern repeats for secondary, tertiary, error, surface, and outline groups.

WCAG Accessibility Guards

The system doesn't just hope for accessibility — it enforces it:

  • Saturation is clamped per color group (e.g., primary: 5%–75%) to prevent washed-out or over-saturated extremes.
  • Lightness is dynamically adjusted — high saturation colors get darker lightness values so white text stays readable.
  • Container saturation is reduced (85% of the main color in light mode, 55% in dark) to keep containers visually recessive.
  • Dark mode boosts minimum lightness proportionally to saturation, ensuring vibrant colors stay legible on dark backgrounds.

This means you can throw basically any hue/saturation combo at it and the output will be accessible. Not 100% of edge cases — but ~99.99% of reasonable inputs.

Customizing

Change two variables, everything updates:

:root {
  --brand-hue: 160;       /* switch to teal */
  --brand-saturation: 50%;
}

For per-section theming, use the [data-custom-theme] attribute on any container:

<section data-custom-theme style="--brand-hue: 340; --brand-saturation: 70%;">
  <!-- this section gets a pink theme -->
</section>

Dark mode is handled automatically via [data-theme="dark"] — all tone values flip, saturation ratios adjust, and container lightness inverts. No extra work.

Source

The complete token generation lives in a single CSS file:

style/_base/colors.css

Further Reading