Skip to content

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 */
ParameterRangeEffect
Lightness0–100%Black → White
Chroma0–0.4Gray → Saturated
Hue0–360Color 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:

StepLightnessRole
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 prefixProperty
.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 200

Output (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 */
}