Color
A closed, generated palette in OKLCH. Twelve steps per hue, one job per step, and contrast that is solved — not hoped for.
prfct's color system is small on purpose. There is no blue-500 to reach for and no hex code to paste. Every color in the product comes from a 12-step scale, and every step exists to do one specific job. Pick the job, and the step picks itself.
Principles
Monochrome by default. Surfaces, text and primary actions are neutral. A calm canvas lets the few colored elements carry meaning instead of competing for attention.
Color is a signal, not a decoration. prfct reserves hue for three jobs:
- Brand marks state and selection: checked boxes, active switches, progress, focus rings, links.
- Status communicates outcomes: success, warning, danger and information.
- Categorical hues distinguish series in charts and tags — never meaning.
Never color alone. Every state that is shown with color is also shown with a shape, an icon or text, so it survives color blindness, grayscale printing and forced-colors mode (WCAG 1.4.1).
Scales
Ten scales ship with prfct: a neutral gray, a themeable brand, four status hues and four categorical hues. Both modes are rendered below from the same stylesheet.
Click any swatch to copy its token. Both modes are rendered live from the same CSS — every panel is just a scoped .light or .dark subtree.
The scales are generated in OKLCH, a perceptually uniform color space: equal steps in lightness look equal, across every hue. That is why a warning amber and an info blue at the same step sit at the same visual weight — something HSL cannot do.
Anatomy of a scale
Each of the twelve steps has a single role. Components never pick steps by eye; they pick them by role.
- Backgrounds
- 1 · 2
- App canvas and subtle surfaces such as sidebars, wells and striped rows.
- Component fills
- 3 · 4 · 5
- Resting, hovered and pressed or selected fills of interactive elements.
- Borders
- 6 · 7 · 8
- Subtle separators, default component borders, and strong or hovered borders.
- Solids
- 9 · 10
- The purest expression of the hue — filled buttons, badges, indicators — and its hover.
- Text
- 11 · 12
- Low-contrast text (≥ 4.5:1 on every fill, steps 1–5) and high-contrast text (≥ 11:1).
Semantic tokens
Most of the time you won't touch scale steps at all. Semantic tokens name the purpose of a color, and map to a step in each mode. Their names follow shadcn/ui's convention — background, primary, muted-foreground — so every shadcn-compatible component works with prfct unchanged.
Surfaces
The page canvas.
Recessed areas that frame content: sidebars, wells, code.
Raised surfaces: cards, panels, tiles.
Floating surfaces: menus, popovers, tooltips, dialogs.
Default text and icons.
Actions
The single most important action. Monochrome by default.
Supporting actions and quiet fills.
De-emphasized fills: tracks, skeletons, tab rails.
Hover and highlight fill for items in lists and menus.
Secondary text, descriptions, placeholders. Meets 4.5:1.
Brand
Brand color. Selection, progress, checked states, emphasis.
Inline links.
Text selection highlight.
Keyboard focus indicator. Meets 3:1 against every surface.
Status
Destructive actions and errors. Fills use `danger-9`; text uses `danger-11`.
Positive outcomes.
Caution. Always paired with dark text.
Neutral information.
Lines
Structural borders: cards, dividers, tables. Decorative.
Emphasized structure and hovered outlines.
Boundaries of form controls. Meets 3:1 against the page (WCAG 1.4.11).
Scrim behind modal surfaces.
Data
Series 1.
Series 2.
Series 3.
Series 4.
Series 5.
// Reach for semantic tokens first…
<p className="text-muted-foreground">Last edited 2 hours ago</p>
// …and scale steps when you're building a component that needs a specific role.
<span className="bg-brand-3 text-brand-11">Beta</span>Brand
prfct's signature is ultramarine — a deep, saturated blue inspired by International Klein Blue. It is used sparingly, so it stays meaningful: where you see it, something is selected, active or in focus.
The brand scale is fully themeable. Change its hue and chroma in the theme builder and all twelve steps are regenerated — including their contrast guarantees — while status colors stay put, so success never turns into your brand color.
Status colors
Status hues have semantic aliases, so components express what happened rather than which color to paint:
redambergreenblueUse the same recipe everywhere: step 2 for the background, 6 for the border, 9 for a solid fill (with -contrast as its foreground), 11 for text and icons, 12 for a title on the tinted background. Alerts, badges, toasts and form errors all follow it.
Contrast
Contrast in prfct is a property of the palette, not a review step. When a scale is generated, its text steps are solved numerically against its fills:
- Step 11 reaches at least 4.5:1 on every fill of its own scale — steps 1 to 5, including hover and pressed states — which is WCAG 2.2 AA for body text. In dark mode it also reaches 5:1 on step 3, because halation makes thin light-on-dark text harder to read.
- Step 12 reaches at least 11:1 on step 3.
- Every solid (step 9) ships with a
-contrastforeground that meets 4.5:1: white where possible, a deep tint of the same hue where white fails (amber and orange). - Form control boundaries use the
inputtoken, which meets 3:1 against the page for WCAG 1.4.11 — the part most design systems quietly skip.
The matrix below is computed from the generated palette at build time. If a change ever broke a guarantee, the table would say so — and so would the test suite.
prefers-contrast: more), prfct strengthens borders, control boundaries and secondary text automatically. In Windows High Contrast (forced-colors: active), components defer to system colors and focus rings stay visible because they are drawn with outlines, not shadows.Dark mode
Dark mode is not an inversion. Each scale is generated twice with separate lightness and chroma curves: dark backgrounds are near-black rather than pure black to avoid smearing on OLED screens, tints carry more chroma so they still read as color at low lightness, and text steps are solved against dark fills. Surfaces gain elevation by getting lighter, not by casting shadows nobody can see.
Any subtree can switch modes by adding a class — useful for previews, embedded panels and marketing sections:
<section className="dark">…</section> {/* dark, even on a light page */}
<section className="light">…</section> {/* light, even on a dark page */}Rules
- Don't use color alone. Pair status colors with an icon or text.
- Don't invent steps. If a color seems to be missing, a role is probably being misused — revisit the table above.
- Don't recolor components with
className. Use a variant, or add one to the component so the decision is shared.
For engineers
The palette is data. Scales are defined by a handful of numbers in lib/tokens/palette.ts and generated by lib/color/scale.ts; pnpm tokens writes styles/tokens.css and DTCG JSON for design tools. Tailwind's default palette is removed, so a stray bg-blue-500 fails to exist rather than silently diverging from the system.
export const huePresets = {
ultramarine: { hue: 267, chroma: 0.24, lightness: 0.515, darkLightness: 0.55 },
// …
}