Theme customiser
Start from one of the three presets, adjust it, and export the CSS. The palette is scored live against the same pairs and thresholds that scripts/check-contrast.ts enforces on every commit — so a theme that shows all-green here is one the build would accept.
Colours are edited in OKLCH on purpose. Lightness is the axis contrast actually depends on, so dragging L moves the ratio predictably, where nudging a hex value is guesswork.
All contrast checks pass
Tokens — light
- Background
#ffffff - Foreground
#010101 - Card
#ffffff - Card foreground
#010101 - Popover
#ffffff - Popover foreground
#010101 - Primary
#020202 - Primary foreground
#f4f4f4 - Secondary
#e9e9e9 - Secondary foreground
#020202 - Muted
#e9e9e9 - Muted foreground
#212121 - Accent
#e9e9e9 - Accent foreground
#020202 - Destructive
#cc0001 - Destructive foreground
#f4f4f4 - Border
#c8c8c8 - Input border
#797979 - Focus ring
#171717
Preview
A section heading
Supporting copy, which is where a muted foreground usually stops meeting 4.5:1.
PrimarySecondaryOutline
A form field borderA focused cardContrast — light
The same pairs and thresholds scripts/check-contrast.ts enforces in CI.
| Pair | Ratio | Result |
|---|---|---|
| Body text | 20.90:1 (min 4.5) | Passes |
| Supporting copy | 16.13:1 (min 4.5) | Passes |
| Copy on muted panels | 13.26:1 (min 4.5) | Passes |
| Card text | 20.90:1 (min 4.5) | Passes |
| Popover text | 20.90:1 (min 4.5) | Passes |
| Primary button label | 18.79:1 (min 4.5) | Passes |
| Secondary button label | 17.03:1 (min 4.5) | Passes |
| Accent surface text | 17.03:1 (min 4.5) | Passes |
| Destructive button label | 5.33:1 (min 4.5) | Passes |
| Primary as a surface or icon | 20.72:1 (min 3) | Passes |
| Focus ring | 17.89:1 (min 3) | Passes |
| Focus ring on cards | 17.89:1 (min 3) | Passes |
| Form field border | 4.35:1 (min 3) | Passes |
| Form field border on cards | 4.35:1 (min 3) | Passes |
Export
Paste this into your stylesheet after the Facade tokens, then set data-facade-theme="custom" on <html>.
[data-facade-theme="custom"] {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.505 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.78 0 0);
--ring: oklch(0.45 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.782 0 0);
--accent: oklch(0.371 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.145 0 0);
--border: oklch(1 0 0 / 12%);
--input: oklch(0.74 0 0);
--ring: oklch(0.78 0 0);
}