Colors
Use Olyx OKLCH color roles and component color tokens correctly.
Olyx uses an OKLCH color system inspired by Material color roles. The color file exposes only the role tokens listed here, and component styles should depend only on these tokens.
Seed Variables
:root {
--base-lightness: 0.5;
--base-chroma: 0.18;
--base-hue: 200;
}| Variable | Range | Purpose |
|---|---|---|
--base-hue | 0 to 360 | Primary hue and neutral hue |
--base-chroma | 0 to 0.4 | Accent color intensity |
--base-lightness | 0 to 1 | Accent role lightness calculation |
Harmony modes change secondary and tertiary hue offsets when applied to the root theme:
<html data-harmony="triadic">
...
</html>Supported values are complementary, triadic, analogous, and monochromatic.
Accent Roles
Use each fill token with its matching on-* token.
| Preview | Token | Use |
|---|---|---|
Aa | --color-primary / --color-on-primary | Primary filled actions and strong active indicators |
Aa | --color-primary-container / --color-on-primary-container | Selected tonal states and active containers |
Aa | --color-secondary / --color-on-secondary | Secondary filled accents |
Aa | --color-secondary-container / --color-on-secondary-container | Secondary tonal containers |
Aa | --color-tertiary / --color-on-tertiary | Tertiary accents |
Aa | --color-tertiary-container / --color-on-tertiary-container | Tertiary tonal containers |
State Roles
State roles follow the same role pairing.
| Preview | Token | Use |
|---|---|---|
Aa | --color-success / --color-on-success | Success fills |
Aa | --color-success-container / --color-on-success-container | Success badges, banners, and alerts |
Aa | --color-info / --color-on-info | Informational fills |
Aa | --color-info-container / --color-on-info-container | Informational badges, banners, and alerts |
Aa | --color-warning / --color-on-warning | Warning fills |
Aa | --color-warning-container / --color-on-warning-container | Warning badges, banners, and alerts |
Aa | --color-error / --color-on-error | Error and destructive fills |
Aa | --color-error-container / --color-on-error-container | Error messages and invalid state backgrounds |
Surface Roles
Surface roles are the neutral system used for component backgrounds, borders, dividers, disabled states, and lower-emphasis text.
| Preview | Token | Use |
|---|---|---|
Aa | --color-surface / --color-on-surface | Default page and component surface |
Aa | --color-surface-container-lowest | Menus, popovers, and lowest container elevation |
Aa | --color-surface-container-low | Hover state and low-emphasis containers |
Aa | --color-surface-container | Disabled fills and grouped neutral containers |
Aa | --color-surface-container-high | Strong hover states and raised neutral containers |
Aa | --color-surface-container-highest | Borders, dividers, and highest neutral elevation |
Aa | --color-surface-variant / --color-on-surface-variant | Alternative neutral surfaces and subdued text/icons |
Component Rules
| Need | Use |
|---|---|
| Text/icons on a filled role | Matching --color-on-* token |
| Selected tonal state | --color-primary-container + --color-on-primary-container |
| Subtle border or divider | --color-surface-container-highest |
| Strong neutral icon or disabled indicator | --color-on-surface-variant |
| Focus ring | color-mix(in oklab, var(--color-primary) 35%, transparent) |
| Error focus ring | color-mix(in oklab, var(--color-error) 35%, transparent) |
| Disabled fill | --color-surface-container |
| Disabled content | color-mix(in srgb, var(--color-on-surface) 38%, transparent) |
| Overlay | color-mix(in srgb, var(--color-on-surface) 32%, transparent) |
| Tooltip dark variant | --color-on-surface background with --color-surface text |
Source
The token generation lives in the shared color files:
:root,
[data-custom-theme] {
/* Base — OKLCH values
L: Lightness [0 - 1]
C: Chroma [0 | 0.4]
H: Hue [0 - 360] */
--base-lightness: 0.5;
--base-chroma: 0.18;
--base-hue: 265;
/* Harmony offsets (°) */
&[data-harmony="complementary"] {
--secondary-offset: 180;
--tertiary-offset: 150;
}
&[data-harmony="triadic"] {
--secondary-offset: 120;
--tertiary-offset: 240;
}
&[data-harmony="analogous"] {
--secondary-offset: 30;
--tertiary-offset: -30;
}
/* Hue */
--primary-h: var(--base-hue);
--secondary-h: calc(var(--base-hue) + var(--secondary-offset, 0));
--tertiary-h: calc(var(--base-hue) + var(--tertiary-offset, 0));
--neutral-h: var(--base-hue);
--error-h: 25;
--success-h: 150;
--info-h: 260;
--warning-h: 48;
/* Chroma */
--neutral-c: 0;
--primary-c: clamp(0.01, var(--base-chroma), 0.32);
--secondary-c: clamp(0.01, calc(var(--base-chroma) * 0.9), 0.3);
--tertiary-c: clamp(0.01, calc(var(--base-chroma) * 0.85), 0.28);
--success-c: 0.19;
--error-c: 0.21;
--info-c: 0.19;
--warning-c: 0.19;
&[data-harmony="monochromatic"] {
--secondary-c: clamp(0.01, calc(var(--base-chroma) * 0.6), 0.28);
--tertiary-c: clamp(0.01, calc(var(--base-chroma) * 0.3), 0.24);
}
/* Chroma + Hue */
--primary-ch: var(--primary-c) var(--primary-h);
--secondary-ch: var(--secondary-c) var(--secondary-h);
--tertiary-ch: var(--tertiary-c) var(--tertiary-h);
--neutral-ch: var(--neutral-c) var(--neutral-h);
--error-ch: var(--error-c) var(--error-h);
--success-ch: var(--success-c) var(--success-h);
--info-ch: var(--info-c) var(--info-h);
--warning-ch: var(--warning-c) var(--warning-h);
/* Lightness */
--primary-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--primary-c) * 0.5),
0.55
);
--secondary-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--secondary-c) * 0.5),
0.55
);
--tertiary-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--tertiary-c) * 0.5),
0.55
);
--error-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--error-c) * 0.5),
0.55
);
--success-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--success-c) * 0.5),
0.55
);
--info-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--info-c) * 0.5),
0.55
);
--warning-l-max: clamp(
0.25,
calc(var(--base-lightness) - var(--warning-c) * 0.5),
0.55
);
/* Roles */
--color-primary: oklch(var(--primary-l-max) var(--primary-ch));
--color-secondary: oklch(var(--secondary-l-max) var(--secondary-ch));
--color-tertiary: oklch(var(--tertiary-l-max) var(--tertiary-ch));
--color-error: oklch(var(--error-l-max) var(--error-ch));
--color-success: oklch(var(--success-l-max) var(--success-ch));
--color-info: oklch(var(--info-l-max) var(--info-ch));
--color-warning: oklch(var(--warning-l-max) var(--warning-ch));
--_on: oklch(1 0 0); /* on-(role) */
--color-on-primary: var(--_on);
--color-on-secondary: var(--_on);
--color-on-tertiary: var(--_on);
--color-on-error: var(--_on);
--color-on-success: var(--_on);
--color-on-info: var(--_on);
--color-on-warning: var(--_on);
/* Containers */
--_cl: 0.93; /* container lightness */
--_cs: 0.55; /* chroma scale */
--color-primary-container: oklch(
var(--_cl) calc(var(--primary-c) * var(--_cs)) var(--primary-h)
);
--color-secondary-container: oklch(
var(--_cl) calc(var(--secondary-c) * var(--_cs)) var(--secondary-h)
);
--color-tertiary-container: oklch(
var(--_cl) calc(var(--tertiary-c) * var(--_cs)) var(--tertiary-h)
);
--color-error-container: oklch(
var(--_cl) calc(var(--error-c) * var(--_cs)) var(--error-h)
);
--color-success-container: oklch(
var(--_cl) calc(var(--success-c) * var(--_cs)) var(--success-h)
);
--color-info-container: oklch(
var(--_cl) calc(var(--info-c) * var(--_cs)) var(--info-h)
);
--color-warning-container: oklch(
var(--_cl) calc(var(--warning-c) * var(--_cs)) var(--warning-h)
);
--_ocl: 0.18; /* on-container lightness */
--color-on-primary-container: oklch(var(--_ocl) var(--primary-ch));
--color-on-secondary-container: oklch(var(--_ocl) var(--secondary-ch));
--color-on-tertiary-container: oklch(var(--_ocl) var(--tertiary-ch));
--color-on-error-container: oklch(var(--_ocl) var(--error-ch));
--color-on-success-container: oklch(var(--_ocl) var(--success-ch));
--color-on-info-container: oklch(var(--_ocl) var(--info-ch));
--color-on-warning-container: oklch(var(--_ocl) var(--warning-ch));
/* Surface */
--color-surface: oklch(0.98 var(--neutral-ch));
--color-on-surface: oklch(0.18 var(--neutral-ch));
--color-surface-container-lowest: oklch(1 var(--neutral-ch));
--color-surface-container-low: oklch(0.965 var(--neutral-ch));
--color-surface-container: oklch(0.945 var(--neutral-ch));
--color-surface-container-high: oklch(0.925 var(--neutral-ch));
--color-surface-container-highest: oklch(0.905 var(--neutral-ch));
--color-surface-variant: oklch(0.91 var(--neutral-ch));
--color-on-surface-variant: oklch(0.38 var(--neutral-ch));
}
[data-theme="dark"],
[data-theme="dark"] [data-custom-theme] {
/* Chroma */
--primary-c: clamp(0.01, calc(var(--base-chroma) * 0.95), 0.36);
--secondary-c: clamp(0.01, calc(var(--base-chroma) * 0.9), 0.34);
--tertiary-c: clamp(0.01, calc(var(--base-chroma) * 0.85), 0.32);
&[data-harmony="monochromatic"] {
--secondary-c: clamp(0.01, calc(var(--base-chroma) * 0.6), 0.32);
--tertiary-c: clamp(0.01, calc(var(--base-chroma) * 0.3), 0.28);
}
/* Lightness */
--primary-l-min: calc(1 - var(--base-lightness) + var(--primary-c) * 0.4);
--secondary-l-min: calc(1 - var(--base-lightness) + var(--secondary-c) * 0.4);
--tertiary-l-min: calc(1 - var(--base-lightness) + var(--tertiary-c) * 0.4);
--error-l-min: calc(1 - var(--base-lightness) + var(--error-c) * 0.4);
--success-l-min: calc(1 - var(--base-lightness) + var(--success-c) * 0.4);
--info-l-min: calc(1 - var(--base-lightness) + var(--info-c) * 0.4);
--warning-l-min: calc(1 - var(--base-lightness) + var(--warning-c) * 0.4);
/* Roles */
--_l-clamp: 0.88, 0.95; /* clamp bounds */
--color-primary: oklch(
clamp(var(--primary-l-min), var(--_l-clamp)) var(--primary-ch)
);
--color-secondary: oklch(
clamp(var(--secondary-l-min), var(--_l-clamp)) var(--secondary-ch)
);
--color-tertiary: oklch(
clamp(var(--tertiary-l-min), var(--_l-clamp)) var(--tertiary-ch)
);
--color-error: oklch(
clamp(var(--error-l-min), var(--_l-clamp)) var(--error-ch)
);
--color-success: oklch(
clamp(var(--success-l-min), var(--_l-clamp)) var(--success-ch)
);
--color-info: oklch(clamp(var(--info-l-min), var(--_l-clamp)) var(--info-ch));
--color-warning: oklch(
clamp(var(--warning-l-min), var(--_l-clamp)) var(--warning-ch)
);
--_on-l: 0.22; /* on lightness */
--color-on-primary: oklch(var(--_on-l) var(--primary-ch));
--color-on-secondary: oklch(var(--_on-l) var(--secondary-ch));
--color-on-tertiary: oklch(var(--_on-l) var(--tertiary-ch));
--color-on-error: oklch(var(--_on-l) var(--error-ch));
--color-on-success: oklch(var(--_on-l) var(--success-ch));
--color-on-info: oklch(var(--_on-l) var(--info-ch));
--color-on-warning: oklch(var(--_on-l) var(--warning-ch));
/* Containers */
--_cl: 0.32; /* container lightness */
--_cs: 0.55; /* chroma scale */
--_ocl: 0.92; /* on-container lightness */
/* Surface */
--color-surface: oklch(0.15 var(--neutral-ch));
--color-on-surface: oklch(0.92 var(--neutral-ch));
--color-surface-container-lowest: oklch(0.1 var(--neutral-ch));
--color-surface-container-low: oklch(0.19 var(--neutral-ch));
--color-surface-container: oklch(0.22 var(--neutral-ch));
--color-surface-container-high: oklch(0.28 var(--neutral-ch));
--color-surface-container-highest: oklch(0.34 var(--neutral-ch));
--color-surface-variant: oklch(0.91 var(--neutral-c) var(--neutral-h) / 15%);
--color-on-surface-variant: oklch(0.81 var(--neutral-ch));
}