- Color Palette
- Accessibility
- Brand Colors
- Design Tokens
From Brand Primary to Accessible Palette: Tints, Shades, and Semantic Roles
Turn one brand seed color into text, surface, border, and semantic tokens you can validate for WCAG contrast and export to CSS or Figma.
Sherotree
·7 min read
Most brand decks hand designers a single hero hex—something like a vivid indigo or coral—and a vague instruction to “build the system around it.” The gap between that seed and a usable product palette is where contrast failures hide: light primary on white for links, soft danger backgrounds with soft danger text, borders that vanish on cards.
This article walks an original seed-to-roles workflow: define the seed, generate tints and shades, assign semantic roles, validate pairs, then export notes for CSS and Figma tokens. All tools linked here are free browser utilities on Uwarp.
Start with one honest seed
Pick the color marketing actually owns—the logo mark, the campaign CTA, or the product illustration accent. Write it down as sRGB hex. Example seed used throughout this article (invented for teaching):
Seed primary: #5B4DFF
Resist the urge to start with five brand colors. One seed plus neutrals plus a small semantic set (danger, warning, success) covers most SaaS and marketing sites. Extra brand hues can wait until the primary scale is readable.
Questions to answer before you generate steps
- Will primary appear as large filled buttons, inline text links, or both?
- Do you need a light theme only, or light + dark?
- Is the product AA (4.5:1 normal text) enough, or do you also target AAA for long-form reading?
If the answer to (1) is “both,” you almost certainly need two primary-related text colors: one that sits on the primary fill, and one that sits on white/gray surfaces as a link color.
Build a scale: tints, mid, shades
A practical product scale is not a perfect HSL ramp. It is a set of steps with jobs:
| Step (example names) | Job |
|---|---|
primary-50 … primary-200 | Soft surfaces, selected rows, chips |
primary-300 … primary-400 | Illustrative accents, large decorative shapes |
primary-500 / seed | Brand recognition on big CTAs |
primary-600 … primary-700 | Stronger fills, hover on light theme |
primary-800 … primary-900 | Text links and small UI on light backgrounds |
Generate candidates with Accessible Brand Colors. Treat the output as a proposal, not a final token list. Your job is to assign roles and kill steps that do not earn a job.
For exploration alongside brand-focused generators, the ColorDesigner curated set and the wider color tools category are useful when you need alternate ramps or supporting utilities—still validate every pair you ship.
Assign semantic roles (not just “more purple”)
Roles beat step numbers in handoff docs. Map colors to meaning:
Core roles
- Text / default — main reading color on surfaces (usually near-black or deep gray, not the brand seed).
- Text / muted — secondary copy; still ≥4.5:1 on the surfaces it uses.
- Surface / default — page background.
- Surface / raised — cards, popovers.
- Border / subtle and Border / strong — structure vs input outlines.
- Primary / fill, Primary / fill-hover, Primary / text-on-fill, Primary / text-on-surface (link).
- Danger / fill, Danger / text, Danger / surface — errors and destructive actions.
- Optional: Warning and Success with the same fill / text / surface split.
Role rule that prevents most failures
Never use the same hex for soft surface and message text. Soft rose backgrounds need a darker red for the sentence “Payment failed.” Soft violet selection rows need a darker violet (or neutral) for the selected label if that label is small.
Validate pairs: accessible-brand-colors + contrast checker
Pass 1 — structural pairs
For each role that includes text or icons, list the background it sits on:
| Foreground role | Background role | Target |
|---|---|---|
| Text / default | Surface / default | ≥4.5:1 (AA normal) |
| Text / muted | Surface / default & raised | ≥4.5:1 |
| Primary / text-on-surface | Surface / default | ≥4.5:1 |
| Primary / text-on-fill | Primary / fill | ≥4.5:1 (button label) |
| Danger / text | Danger / surface and Surface / default | ≥4.5:1 |
| Border / strong (inputs) | Surface / default | aim ~3:1 vs adjacent surface for clarity |
Pass 2 — tool workflow
- Draft the scale in Accessible Brand Colors from your seed.
- Copy the candidate hexes into a scratch table (Notion, spreadsheet, or design tokens file).
- Open the Colour Contrast Checker.
- Test every row in the structural table above.
- When a pair fails, adjust one role at a time: usually darken text-on-surface, or lighten / darken the fill under white labels—not both randomly.
- Re-check hover and focus if those states change fill.
Invented before/after for the sample seed
Before (common mistake):
- Link text:
#5B4DFFon#FFFFFF— may pass or sit close depending on exact computation; on#F5F3FFit often gets worse. - Button: white label on
#5B4DFF— check explicitly; many vivid purples pass for large buttons and still feel harsh for small UI. - Danger text:
#F87171on#FEF2F2— frequently fails AA for body-sized errors.
After (role split):
primary-text-on-surface:#4338CA(darker sibling for links)primary-fill:#5B4DFFprimary-text-on-fill:#FFFFFF(confirm ≥4.5:1)primary-surface:#EEF2FFdanger-text:#B91C1Cdanger-surface:#FEF2F2
Re-run each pair in the contrast checker before you lock tokens.
Neutrals are half the system
Brand scales get attention; neutrals carry reading. Define at least:
neutral-0/ whiteneutral-50–neutral-200surfacesneutral-400–neutral-500borders and disabled chrome (carefully)neutral-700–neutral-900text
Muted text should be a neutral that still clears 4.5:1 on your lightest common surface—not “whatever looks secondary in the mock.”
Export notes for CSS and Figma tokens
CSS custom properties
Name by role, alias steps underneath:
:root {
--color-primary-fill: #5b4dff;
--color-primary-fill-hover: #4f46e5;
--color-primary-text-on-fill: #ffffff;
--color-primary-text-on-surface: #4338ca;
--color-primary-surface: #eef2ff;
--color-text-default: #111827;
--color-text-muted: #4b5563;
--color-surface-default: #ffffff;
--color-surface-raised: #f9fafb;
--color-border-subtle: #e5e7eb;
--color-border-strong: #9ca3af;
--color-danger-text: #b91c1c;
--color-danger-surface: #fef2f2;
}Keep raw ramp variables (--color-primary-500) if engineers want them, but components should consume roles so a contrast fix is one token change.
Figma
- Create a Color collection with modes for Light / Dark if you ship both.
- Mirror the same role names as CSS.
- Document in the file description: “Text-on-surface primary is not the marketing seed; seed is fill.”
- After token updates, spot-check a key frame with the browser contrast checker—plugins help inside Figma, but a second tool catches export typos.
Hand-off checklist
- Seed hex recorded; primary fill and primary text-on-surface are separate tokens.
- Soft surfaces never reuse message text colors.
- AA pairs documented for default text, muted text, primary link, primary button label, danger text.
- CSS and Figma use the same role names.
- Dark mode (if any) re-validated, not only inverted.
A short workflow you can reuse on every brand
- Capture seed → Accessible Brand Colors.
- Assign roles (table above).
- Validate with Colour Contrast Checker.
- Export role-based CSS / Figma tokens.
- Only then expand into secondary brand hues.
That order keeps marketing’s favorite color visible on CTAs without forcing every paragraph link to fight WCAG.
Related tools
Suggested next article
For decorative backgrounds that sit behind this palette, see Choosing Free SVG Background Generators—and always re-check type contrast when blobs or grain land under text.


