Acknowledgement: Shared by Peter Bamuhigire, techguypeter.com.
-
Find a real anchor before picking any hex. Derive the palette from something that
actually exists — a brand mark, a product material, a place, a printed reference, a named
designer's or artist's work. State the anchor and the primary hue before generating the
artifact, exactly as typography states the typeface first (per the Mission in
doctrine/design-doctrine.md §0 and the Anti-Slop Charter §2.1). A palette with no anchor
is how slop happens.
-
Reject the convergent defaults explicitly. Do not ship the indigo/violet→blue gradient,
the unedited Tailwind default scale, frosted-glass panels as the identity, or neon accents
on near-black. These are the colour equivalent of opening in Inter — see
doctrine/references/ai-slop-taxonomy.md. If the anchor genuinely lands near one of these,
shift hue, desaturate, or warm/cool it until it no longer reads as the stock look, and say so.
-
Build the ramp by perceptual steps, not by lightening toward white. Take the anchor hue
and generate a tonal ramp (roughly 50–900) where each step is an even perceptual lightness
move, holding or subtly rotating hue and adjusting chroma so mid-tones don't go muddy and
light tints don't go chalky. This is Albers' lesson in Interaction of Color — a colour is
judged by its neighbours, so tune the ramp in context, not in isolation. Itten's contrast
types (hue, light–dark, saturation, warm–cool) name the levers; pick the one the artifact
needs. Prefer a perceptually-uniform space (OKLCH / CIELAB) over naive HSL, whose "equal"
steps are perceptually uneven. Operationalize this in OKLCH (CSS Color 4, native in evergreen
browsers): author tokens as oklch(L C H) — step the ramp evenly on L (e.g. 50→900 at
L ≈ 0.97→0.24), arc C so it peaks in the mid-tones and falls off both ends (so tints don't
go chalky, shades don't go muddy), and hold H roughly constant (rotate ≤ ~15° only with
intent). Build neutrals as a low-chroma ramp on the anchor hue (C ≈ 0.005–0.02), never pure grey.
The full recipe with stop tables is in references/oklch-ramp-construction.md.
-
Assign semantic roles, not raw hexes. Map ramp positions to roles: surface (and
raised/sunken surfaces), text (primary + muted), border, accent (the brand voice),
and the status set success / warning / error / info. Components reference roles, never
literal colours, so the system can be retuned or remapped in one place. Each role carries the
contrast contract (e.g. text-muted is defined as ≥4.5:1 on surface), so the gate is
applied once per role pairing, not re-litigated per screen. Full role set + bindings in
references/semantic-color-roles.md.
-
Hold the 60-30-10 distribution. ~60% a quiet dominant (usually surface), ~30% a
secondary, ~10% the accent that carries the brand. The accent earns its scarcity — colour
everywhere is colour nowhere. One confident accent beats a rainbow of hedged ones (Mission §0).
-
Gate every foreground/background pair on WCAG — this is a hard gate, not advice.
Body and small text ≥ 4.5:1; large text (≥24px, or ≥18.66px bold) and meaningful UI
components/icons/focus rings ≥ 3:1 (WCAG 2.x 1.4.3 / 1.4.11). Any pair that fails is
rejected and the ramp step is retuned until it passes — no exceptions for "it looks fine."
Design with APCA, certify with WCAG: while tuning mid-tones, judge real legibility with
APCA (the WCAG 3.0 draft algorithm — not yet normative), which models thin/small text
better; then verify against the 4.5:1 / 3:1 ratios for conformance before shipping. See
doctrine/references/wcag-2.2-criteria.md (Contrast method note).
-
Never put pure black on white, or pure white on a saturated field. #000 on #fff
over-contrasts and vibrates (haloing/eye strain); use a near-black with a trace of the brand
hue (e.g. a very dark desaturated version of the anchor). Likewise prefer a near-white,
slightly warm or cool surface over #fff. This is perceptual colour science, not taste.
-
Make dark mode a deliberate remap, not an inversion. Do not invert lightness. Dark
surfaces want lower chroma and a near-dark grey carrying the brand hue (not #000);
accents usually need to be lightened and slightly desaturated to stay legible and keep the
same semantic meaning at 3:1/4.5:1. Re-run the contrast gate (step 6) for the dark roles
independently — passing in light mode does not imply passing in dark. Dark mode is a re-bind
of the same role names, never an inversion: surface points low (≈ ramp 900), surface-raised
becomes lighter than surface (elevation logic flips), text points high, and accent-text
may flip from white to near-black when the accent is lightened. The role names — and the
component code — stay identical; only the ramp bindings change (references/semantic-color-roles.md).
-
State the system and hand off. Output the named roles, the ramp, the distribution rule,
and the recorded contrast results. Hand categorical/sequential data scales to
12-data-viz-and-dashboards/data-visualization.
Read and colour calculation are required. Edit only for authorised token or artefact changes; rendering is preferred. Do not overwrite established brand colours without owner approval.
If required evidence or tooling is unavailable, use the scoped fallback below and mark the result unverified.
Without colour tooling or rendering, return a provisional role architecture and candidate values, marking gamut and contrast unverified. Recover with approved pairs until tests exist.