Appearance
Color System
remCSS uses oklch() for all colors and light-dark() for automatic theme switching. No hex values. No rgb(). No @media (prefers-color-scheme) inside components.
Why oklch()?
oklch (OK Lightness Chroma Hue) is a perceptually uniform color space. Equal numeric steps in oklch look like equal visual steps to human eyes — unlike hsl where equal numeric steps can produce wildly different perceptual jumps.
css
/* oklch(lightness% chroma hue) */
--color-accent: oklch(62% 0.22 27); /* remCSS orange */| Parameter | Range | Effect |
|---|---|---|
| Lightness | 0–100% | Black → White |
| Chroma | 0–0.4 | Gray → Saturated |
| Hue | 0–360 | Color wheel degrees |
The light-dark() Function
Instead of separate @media (prefers-color-scheme: dark) blocks, remCSS uses light-dark():
css
/* One declaration handles both modes */
color: light-dark(var(--color-on-surface-light), var(--color-on-surface-dark));
background: light-dark(var(--color-surface-light), var(--color-surface-dark));light-dark() is activated by color-scheme: light dark on :root, set in the reset. The browser reads the user's OS preference and picks the correct value automatically.
Token Structure
Palette (raw oklch values)
css
:root {
/* Neutral */
--palette-neutral-0: oklch(98% 0 0); /* near white */
--palette-neutral-100: oklch(10% 0 0); /* near black */
/* Accent — remCSS orange (#fa572a ≈ oklch(62% 0.22 27)) */
--palette-accent-50: oklch(72% 0.22 27); /* lighter */
--palette-accent: oklch(62% 0.22 27); /* base */
--palette-accent-70: oklch(52% 0.22 27); /* darker */
/* Semantic states */
--palette-success: oklch(60% 0.15 145);
--palette-warning: oklch(75% 0.17 85);
--palette-danger: oklch(55% 0.22 22);
--palette-info: oklch(60% 0.18 250);
}Semantic Tokens (light-dark pairs)
css
:root {
--color-surface: light-dark(var(--palette-neutral-0), var(--palette-neutral-100));
--color-on-surface: light-dark(var(--palette-neutral-100), var(--palette-neutral-0));
--color-accent: light-dark(var(--palette-accent), var(--palette-accent-50));
--color-border: light-dark(oklch(85% 0 0), oklch(25% 0 0));
}Named Color Palette
remCSS ships 12 named color families, each with 6 steps. All values are oklch().
10
30
50
70
90
95
red 22°
amber 55°
yellow 90°
lime 125°
green 145°
teal 175°
sky 210°
blue 230°
indigo 265°
violet 285°
purple 300°
pink 340°
Step semantics:
| Step | Lightness | Role |
|---|---|---|
| 10 | ~12–18% | Dark background tint / dark-mode text |
| 30 | ~35–40% | Dark-mode foreground, WCAG AA on white |
| 50 | ~60–78% | Vivid base — the "brand" step |
| 70 | ~75–82% | Light-mode foreground, subtle on dark bg |
| 90 | ~90–93% | Soft tinted surface |
| 95 | ~94–96% | Near-white tint, barely-there background |
Semantic tokens per color
For every named family, three semantic tokens are available:
css
--color-{name} /* theme-aware: dark text in light mode, light text in dark mode */
--color-{name}-subtle /* theme-aware tinted surface */
--color-on-{name} /* foreground on a step-50 (vivid) background */Color utility classes
html
<span class="text-blue">Blue text</span>
<div class="bg-green-subtle">Tinted surface</div>
<button class="button bg-red text-on-red">Delete</button>Available for all 12 families + accent, success, warning, danger, info, muted:
| Class prefix | Property |
|---|---|
.text-{name} | color |
.bg-{name} | background-color |
.border-{name} | border-color |
Dynamic Palette Slots
remCSS includes 3 ready-made slots for custom colors. Set a single CSS variable and the framework derives a full 6-step scale automatically at runtime — no build step, no script.
css
/* In your own CSS, after importing remcss.css */
:root {
--palette-custom-1-base: oklch(62% 0.22 200); /* one line → 6 steps */
}This automatically gives you:
css
--palette-custom-1-10 /* darkest — ~15% L */
--palette-custom-1-30 /* dark — ~38% L */
--palette-custom-1-50 /* = your base color */
--palette-custom-1-70 /* light — ~76% L */
--palette-custom-1-90 /* lighter — ~92% L */
--palette-custom-1-95 /* lightest — ~95% L */
--color-custom-1 /* light-dark() semantic token */
--color-custom-1-subtle /* light-dark() subtle surface */
--color-on-custom-1 /* foreground on vivid background */And the utility classes .text-custom-1, .bg-custom-1, .border-custom-1.
Three slots are available: custom-1, custom-2, custom-3. Defaults:
- Slot 1:
oklch(60% 0.2 240)— blue - Slot 2:
oklch(60% 0.2 340)— rose - Slot 3:
oklch(60% 0.2 145)— green
How it works
The derivation uses color-mix() in oklch space against near-black and near-white anchors with a none hue — which causes the base hue to propagate through all steps:
css
--palette-custom-1-10: color-mix(in oklch, var(--palette-custom-1-base) 8%, oklch(12% 0 none));
--palette-custom-1-30: color-mix(in oklch, var(--palette-custom-1-base) 55%, oklch(12% 0 none));
--palette-custom-1-50: var(--palette-custom-1-base);
--palette-custom-1-70: color-mix(in oklch, var(--palette-custom-1-base) 55%, oklch(97% 0 none));
--palette-custom-1-90: color-mix(in oklch, var(--palette-custom-1-base) 15%, oklch(97% 0 none));
--palette-custom-1-95: color-mix(in oklch, var(--palette-custom-1-base) 5%, oklch(97% 0 none));The none hue on oklch(12% 0 none) means the channel is "missing" — color-mix() uses the other color's hue. Result: all steps share the exact hue of your base color.
Bright bases
If your base color is very light (> 72% L), override the on-color token with dark text:
css
:root {
--color-on-custom-1: oklch(10% 0 0);
}Palette Generator Script
For permanent named palettes — or if you need more than 3 custom slots — use scripts/gen-palette.js to generate ready-to-paste CSS from one oklch value:
bash
# From an oklch string
node scripts/gen-palette.js coral "oklch(65% 0.19 22)"
# From individual L C H values
node scripts/gen-palette.js brand 62 0.22 200Output (two copy-paste blocks):
css
/* ── Block 1: paste into src/tokens/colors.css ──────────────────── */
/* coral — generated 2026-05-12 */
--palette-coral-10: oklch(16.1% 0.036 22);
--palette-coral-30: oklch(37.6% 0.105 22);
--palette-coral-50: oklch(65% 0.19 22);
--palette-coral-70: oklch(77.3% 0.105 22);
--palette-coral-90: oklch(91.8% 0.029 22);
--palette-coral-95: oklch(95.6% 0.01 22);
--color-coral: light-dark(var(--palette-coral-30), var(--palette-coral-70));
--color-coral-subtle: light-dark(var(--palette-coral-95), var(--palette-coral-10));
--color-on-coral: oklch(98% 0 0);
/* ── Block 2: paste into src/utilities/colors.css ───────────────── */
.text-coral {
color: var(--color-coral);
}
.bg-coral {
background-color: var(--color-coral-subtle);
}
.border-coral {
border-color: var(--color-coral);
}The script uses the same mix percentages as the CSS slots, so static and dynamic palettes look identical at equivalent base colors.
Defining Custom Colors (Manual)
For full control without the generator script, follow the two-layer pattern directly:
css
:root {
/* 1. Define your palette */
--palette-brand-30: oklch(38% 0.19 195);
--palette-brand-50: oklch(58% 0.19 195);
--palette-brand-70: oklch(76% 0.1 195);
--palette-brand-95: oklch(95% 0.03 195);
/* 2. Create semantic tokens */
--color-brand: light-dark(var(--palette-brand-30), var(--palette-brand-70));
--color-brand-subtle: light-dark(var(--palette-brand-95), var(--palette-brand-30));
--color-on-brand: oklch(98% 0 0);
}Forcing a Color Scheme
You can force light or dark mode on any element:
html
<!-- Force dark regardless of OS preference -->
<div style="color-scheme: dark">
<!-- light-dark() here returns the dark value -->
</div>Or globally in CSS:
css
:root {
color-scheme: dark; /* or: light */
}