Skip to content

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.

Deploy previewReady
x

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.

123456789101112
gray
brand
red
amber
green
blue
orange
teal
violet
pink
123456789101112
gray
brand
red
amber
green
blue
orange
teal
violet
pink

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.

1
2
3
4
5
6
7
8
9
10
11
12
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).
StepsRoleTypical classes
1–2App and subtle backgroundsbg-gray-1, bg-brand-2
3–5Component fills — rest, hover, pressedbg-gray-3 hover:bg-gray-4 active:bg-gray-5
6–8Borders — subtle, default, strongborder-gray-6, hover:border-gray-8
9–10Solid fills and their hoverbg-brand-9 hover:bg-brand-10
11Low-contrast text, icons, links — AA on steps 1–5text-brand-11
12High-contrast texttext-gray-12

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.

whitegray-1

Recessed areas that frame content: sidebars, wells, code.

gray-1gray-1

Raised surfaces: cards, panels, tiles.

whitegray-2

Floating surfaces: menus, popovers, tooltips, dialogs.

whitegray-2

Default text and icons.

gray-12gray-12

Actions

The single most important action. Monochrome by default.

gray-12gray-12

Supporting actions and quiet fills.

gray-3gray-3

De-emphasized fills: tracks, skeletons, tab rails.

gray-3gray-3

Hover and highlight fill for items in lists and menus.

gray-3gray-4

Secondary text, descriptions, placeholders. Meets 4.5:1.

gray-11gray-11

Brand

Brand color. Selection, progress, checked states, emphasis.

brand-9brand-9

Inline links.

brand-11brand-11

Text selection highlight.

brand-5brand-6

Keyboard focus indicator. Meets 3:1 against every surface.

brand-9brand-10

Status

Destructive actions and errors. Fills use `danger-9`; text uses `danger-11`.

red-9red-10

Positive outcomes.

green-9green-9

Caution. Always paired with dark text.

amber-9amber-9

Neutral information.

blue-9blue-9

Lines

Structural borders: cards, dividers, tables. Decorative.

gray-6gray-6

Emphasized structure and hovered outlines.

gray-7gray-7

Boundaries of form controls. Meets 3:1 against the page (WCAG 1.4.11).

gray-9gray-9

Scrim behind modal surfaces.

gray-12 @ 32%black @ 60%

Data

Series 1.

brand-9brand-10

Series 2.

teal-9teal-9

Series 3.

violet-9violet-10

Series 4.

orange-9orange-9

Series 5.

pink-9pink-10
// 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:

danger-1…12Alias of red
warning-1…12Alias of amber
success-1…12Alias of green
info-1…12Alias of blue
Deployedsuccess-2 · 6 · 9 · 11 · 12
Degradedwarning-2 · 6 · 9 · 11 · 12
Faileddanger-2 · 6 · 9 · 11 · 12
Queuedinfo-2 · 6 · 9 · 11 · 12

Use 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.

Payment failed
Do.Status text uses step 11, which is solved for AA on its own tints.
Payment failed
Don’t.Solid step 9 as text fails contrast on tinted backgrounds, especially in dark mode.

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 -contrast foreground 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 input token, 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.

Scale11 on 111 on 211 on 311 on 411 on 512 on 3Contrast on 9
gray5.86AA5.65AA5.31AA4.97AA4.63AA15.0Lc 965.38AA
gray9.32AA8.81AA8.17AA7.53AA6.88AA14.8Lc 965.28AA
brand6.27AA6.05AA5.70AA5.32AA4.87AA11.7Lc 916.07AA
brand8.08AA7.65AA6.67AA5.88AA5.05AA12.1Lc 875.20AA
red6.00AA5.78AA5.44AA5.05AA4.60AA11.9Lc 914.93AA
red7.70AA7.32AA6.43AA5.69AA4.91AA12.1Lc 864.93AA
amber5.93AA5.73AA5.41AA5.04AA4.60AA11.7Lc 919.66AA
amber8.02AA7.60AA6.58AA5.74AA4.92AA12.0Lc 869.66AA
green5.82AA5.64AA5.33AA5.00AA4.60AA11.3Lc 914.74AA
green8.51AA8.04AA6.87AA5.91AA5.01AA12.0Lc 874.74AA
blue5.89AA5.70AA5.38AA5.02AA4.60AA11.6Lc 914.70AA
blue8.20AA7.76AA6.71AA5.85AA5.00AA12.0Lc 874.70AA
orange5.98AA5.77AA5.43AA5.05AA4.60AA11.8Lc 915.56AA
orange7.82AA7.42AA6.50AA5.71AA4.92AA12.1Lc 865.56AA
teal5.84AA5.65AA5.34AA5.01AA4.60AA11.4Lc 914.60AA
teal8.43AA7.97AA6.81AA5.87AA4.99AA12.0Lc 874.60AA
violet5.97AA5.76AA5.42AA5.05AA4.60AA11.9Lc 925.72AA
violet7.83AA7.43AA6.52AA5.76AA4.97AA12.1Lc 865.72AA
pink6.03AA5.81AA5.46AA5.07AA4.60AA11.9Lc 914.67AA
pink7.69AA7.30AA6.42AA5.68AA4.91AA12.1Lc 864.67AA
Increased contrast
When the operating system asks for more contrast (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

mutedbrand-3/11
Do.Semantic tokens and scale steps — the palette stays coherent and themeable.
#6366f1
Don’t.Raw hex and arbitrary values break dark mode, theming and contrast guarantees.
  • 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.

lib/tokens/palette.ts
export const huePresets = {
  ultramarine: { hue: 267, chroma: 0.24, lightness: 0.515, darkLightness: 0.55 },
  // …
}