design token
Color system with five base palettes and semantic tokens that adapt to light and dark themes.
The Pindoba color system uses custom color ramps stored as W3C Design Tokens (DTCG format) with automatic light and dark mode adaptation. Five base palettes feed into five semantic color groups.
Five base color families, each with 11 shades (50–950). Each family has independent light and dark ramps; semantic tokens pick the right one automatically.
Every group exposes the same shape: solid fills, text steps, borders, and a five-step surface micro-ramp. Reach for these by intent rather than raw shade.
Color ramps are generated in the OKLCH color space using a contrast-based approach:
Text tokens are contrast-solved against the worst-case surface — both the neutral and own-family surface.ground — so text always meets its target ratio regardless of which surface it sits on.
Surfaces are a 5-stop micro-ramp between shade 50 and shade 100, named as a terrain ladder that reads as structural depth (never as shadow elevation — shadows are a separate axis that composes with any surface):
| Stop | Value | Role |
|---|---|---|
surface.peak | pegged to shade 50 (light) / 100 (dark) | the topmost color: panels, cards, and — with a shadow — popovers and menus |
surface.hill | 25% between the endpoints | intermediate raised tier |
surface.base | midpoint | the page canvas |
surface.valley | 75% between the endpoints | insets, disabled fills |
surface.ground | pegged to shade 100 (light) / 50 (dark) | the app floor: sidebars, window chrome |
The endpoints are pegged — byte-identical to their ramp shades and emitted as shade refs, so overriding a base ramp propagates to both ends of the band. Only the three middle stops accept generator surface offsets; the endpoints are tuned through the shade curve itself.
The accent band (accent.surface.*, interactive fills rest → hover → active → pressed) follows the same peg rule: accent.surface.peak = shade 500 and accent.surface.ground = the adjacent anchor shade (600 light / 400 dark, in the direction away from text.contrast.*), both as shade refs — one ramp interval quartered, mirroring the structural band. When a pegged dark ground sits below the ≥2:1 presence floor against the page, the generator raises an accentPresenceTooLow warning (surfaced in devtools) instead of moving the band — retune the far anchor shade’s curve to fix it.
Migrating from the old names:
surface.soft→surface.peak,surface.step.1→surface.hill,surface.step.2→surface.base,surface.step.3→surface.valley,surface.deep→surface.ground(same mapping underaccent.surface.*).
Per-mode palettes: light and dark modes have independent color family inputs. A lightness reference family (default: blue) shares its solved lightness curve with all other families, keeping them visually consistent. Neutral (gray) always uses its own independent lightness curve.
Two fixed contrast values are available for cases where you need guaranteed light or dark:
contrast.darkest; // #181818
contrast.lightest; // #f9fbff
import { css } from "@pindoba/styled-system/css";
// Semantic surface and text
<div className={css({
backgroundColor: "neutral.surface.peak",
color: "neutral.text.bold",
})}>
Card content
</div>
// Solid fill button — interactive states step through the accent band
// (rest = accent.surface.peak = shade 500; the same text color passes on
// every band stop by construction)
<button className={css({
backgroundColor: "primary.accent.surface.peak",
color: "primary.text.contrast",
_hover: { backgroundColor: "primary.accent.surface.hill" },
_active: { backgroundColor: "primary.accent.surface.base" },
})}>
Submit
</button>
// Subtle surface button
<button className={css({
backgroundColor: "primary.surface.peak",
color: "primary.text",
_hover: { backgroundColor: "primary.surface.base" },
_active: { backgroundColor: "primary.surface.ground" },
})}>
Secondary action
</button>
// Status feedback
<div className={css({
backgroundColor: "danger.surface.peak",
color: "danger.text.bold",
borderWidth: "1px",
borderColor: "danger.border.muted",
})}>
Error message
</div>
The colorPalette utility lets components switch between semantic groups without duplicating styles:
<div className={css({ colorPalette: "success" })}>
<span className={css({ color: "colorPalette.text.bold" })}>
Status text
</span>
</div>
<div className={css({ colorPalette: "danger" })}>
<span className={css({ color: "colorPalette.text.bold" })}>
Error text
</span>
</div>
primary.surface.peak, neutral.text.bold) rather than raw base colors (blue.light.500).text.contrast for text on solid-colored backgrounds (primary, primary.accent.surface.*) — it passes on every accent-band stop by constructionaccent.surface.peak, hover accent.surface.hill, active accent.surface.base (there are no dedicated hover/active tokens).text.muted for helper text and secondary content.border.muted for subtle separators and .border.bold for emphasissurface.peak for light backgrounds and surface.ground for deeper emphasis — they automatically adapt to the active theme mode.border (30%), .border.muted (15%), .border.bold (65%)