Palette: B2B SaaS — violet primary¶
Seed:
#7C3AED(violet-600) · Mood: modern, trustworthy, mildly creative · Target: Tailwind v4 + shadcn-ui CSS vars Generated by:skills/color-paletteskill
Why this palette¶
A B2B SaaS analytics product needs to read as competent (cool blue-violet hue), modern (contemporary chroma) and calm (low-chroma neutrals). Violet over blue differentiates from the default-Tailwind-blue trap that 60% of SaaS sites are stuck in, while staying in the "trustworthy cool" family.
Tradeoff: violet at chroma 0.28 is slightly punchier than the safest indigo. Acceptable for a product that wants brand recognition; if "boring/safe" is the goal, swap to indigo-600 (#4F46E5).
Tokens¶
Brand ramps (OKLCH-derived)¶
| Step | Primary (violet) | Accent (amber) | Neutral (cool gray) |
|---|---|---|---|
| 50 | #F5F3FF |
#FFFBEB |
#F8FAFC |
| 100 | #EDE9FE |
#FEF3C7 |
#F1F5F9 |
| 200 | #DDD6FE |
#FDE68A |
#E2E8F0 |
| 300 | #C4B5FD |
#FCD34D |
#CBD5E1 |
| 400 | #A78BFA |
#FBBF24 |
#94A3B8 |
| 500 | #8B5CF6 |
#F59E0B |
#64748B |
| 600 | #7C3AED |
#D97706 |
#475569 |
| 700 | #6D28D9 |
#B45309 |
#334155 |
| 800 | #5B21B6 |
#92400E |
#1E293B |
| 900 | #4C1D95 |
#78350F |
#0F172A |
| 950 | #2E1065 |
#451A03 |
#020617 |
(Sourced from Tailwind v4 OKLCH-derived ramps — violet, amber, slate. Each step is computed in OKLCH for perceptually uniform lightness.)
Semantic aliases — light mode¶
| Token | Value | Notes |
|---|---|---|
--color-primary-default |
#7C3AED |
violet-600 — 5.27:1 on white ✓ |
--color-primary-hover |
#6D28D9 |
violet-700 |
--color-primary-active |
#5B21B6 |
violet-800 |
--color-primary-subtle-bg |
#F5F3FF |
violet-50 — for hover backgrounds, badges |
--color-on-primary |
#FFFFFF |
white text on primary |
--color-bg-default |
#FFFFFF |
|
--color-bg-elevated |
#F8FAFC |
slate-50 — cards, popovers |
--color-bg-subtle |
#F1F5F9 |
slate-100 — section backgrounds |
--color-text-primary |
#0F172A |
slate-900 — 18.7:1 on white |
--color-text-secondary |
#475569 |
slate-600 — 7.4:1 |
--color-text-tertiary |
#64748B |
slate-500 — 4.9:1 (passes AA body) |
--color-text-disabled |
#94A3B8 |
slate-400 — 2.7:1 (intentional, with aria-disabled) |
--color-border-default |
#E2E8F0 |
slate-200 |
--color-border-strong |
#CBD5E1 |
slate-300 |
--color-focus-ring |
#A78BFA |
violet-400 — clears 3:1 on white AND on violet-600 |
--color-success |
#16A34A |
green-600 — 4.5:1 on white |
--color-success-subtle-bg |
#F0FDF4 |
green-50 |
--color-warning |
#D97706 |
amber-600 — 4.7:1 on white |
--color-warning-subtle-bg |
#FFFBEB |
amber-50 |
--color-error |
#DC2626 |
red-600 — 4.8:1 on white |
--color-error-subtle-bg |
#FEF2F2 |
red-50 |
--color-info |
#7C3AED |
matches primary |
Semantic aliases — dark mode¶
Recomputed, not inverted. Increased chroma on accents (low-light eye is less saturated). Background uses near-black with cool tint.
| Token | Value | Notes |
|---|---|---|
--color-primary-default |
#A78BFA |
violet-400 — reads against dark bg |
--color-primary-hover |
#C4B5FD |
violet-300 |
--color-primary-active |
#DDD6FE |
violet-200 |
--color-primary-subtle-bg |
#2E1065 |
violet-950 |
--color-on-primary |
#1E1B4B |
very dark violet |
--color-bg-default |
#020617 |
slate-950 |
--color-bg-elevated |
#0F172A |
slate-900 — for cards |
--color-bg-subtle |
#1E293B |
slate-800 |
--color-text-primary |
#F8FAFC |
slate-50 — 17.4:1 on bg-default |
--color-text-secondary |
#CBD5E1 |
slate-300 — 11.6:1 |
--color-text-tertiary |
#94A3B8 |
slate-400 — 6.5:1 |
--color-text-disabled |
#64748B |
slate-500 — 3.4:1 |
--color-border-default |
#334155 |
slate-700 |
--color-border-strong |
#475569 |
slate-600 |
--color-focus-ring |
#A78BFA |
violet-400 |
--color-success |
#4ADE80 |
green-400 |
--color-warning |
#FBBF24 |
amber-400 |
--color-error |
#F87171 |
red-400 |
--color-info |
#A78BFA |
violet-400 |
Contrast matrix (light)¶
| Pair | Ratio | AA body | AA UI | AAA |
|---|---|---|---|---|
| text-primary on bg-default | 18.7:1 | ✓ | — | ✓ |
| text-secondary on bg-default | 7.4:1 | ✓ | — | ✓ |
| text-tertiary on bg-default | 4.9:1 | ✓ | — | — |
| primary-default on bg-default | 5.27:1 | ✓ | — | — |
| on-primary on primary-default | 8.0:1 | ✓ | — | ✓ |
| border-default on bg-default | 1.6:1 | — | (decorative) | — |
| border-strong on bg-default | 2.0:1 | — | (decorative) | — |
| focus-ring on bg-default | 3.4:1 | — | ✓ | — |
| focus-ring on primary-default | 3.1:1 | — | ✓ | — |
| success on bg-default | 4.5:1 | ✓ | — | — |
| warning on bg-default | 4.7:1 | ✓ | — | — |
| error on bg-default | 4.8:1 | ✓ | — | — |
Contrast matrix (dark)¶
| Pair | Ratio | AA body | AA UI |
|---|---|---|---|
| text-primary on bg-default | 17.4:1 | ✓ | — |
| text-secondary on bg-default | 11.6:1 | ✓ | — |
| text-tertiary on bg-default | 6.5:1 | ✓ | — |
| primary-default on bg-default | 6.7:1 | ✓ | — |
| on-primary on primary-default | 8.6:1 | ✓ | — |
| border-default on bg-default | 4.0:1 | — | ✓ |
| focus-ring on bg-default | 8.7:1 | — | ✓ |
Output: Tailwind v4¶
@import "tailwindcss";
@theme {
/* Brand */
--color-primary-50: oklch(0.969 0.016 293.756);
--color-primary-100: oklch(0.943 0.029 294.588);
--color-primary-200: oklch(0.894 0.057 293.283);
--color-primary-300: oklch(0.811 0.111 293.571);
--color-primary-400: oklch(0.702 0.183 293.541);
--color-primary-500: oklch(0.606 0.250 292.717);
--color-primary-600: oklch(0.541 0.281 293.009);
--color-primary-700: oklch(0.491 0.270 292.581);
--color-primary-800: oklch(0.432 0.232 292.759);
--color-primary-900: oklch(0.381 0.176 293.745);
--color-primary-950: oklch(0.283 0.141 291.089);
/* Accent */
--color-accent-50: oklch(0.987 0.022 95.277);
--color-accent-500: oklch(0.769 0.188 70.080);
--color-accent-600: oklch(0.666 0.179 58.318);
/* Neutrals */
--color-neutral-50: oklch(0.984 0.003 247.858);
--color-neutral-200: oklch(0.929 0.013 255.508);
--color-neutral-500: oklch(0.554 0.046 257.417);
--color-neutral-900: oklch(0.208 0.042 265.755);
--color-neutral-950: oklch(0.129 0.042 264.695);
}
Output: shadcn-ui CSS vars¶
:root {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--card: 210 40% 98%;
--card-foreground: 222 47% 11%;
--popover: 0 0% 100%;
--popover-foreground: 222 47% 11%;
--primary: 263 70% 50%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
--accent: 32 95% 44%;
--accent-foreground: 0 0% 100%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 258 90% 66%;
--radius: 0.5rem;
}
.dark {
--background: 222 47% 4%;
--foreground: 210 40% 98%;
--card: 222 47% 11%;
--card-foreground: 210 40% 98%;
--primary: 258 90% 76%;
--primary-foreground: 247 67% 20%;
--muted: 217 33% 17%;
--muted-foreground: 215 20% 65%;
--accent: 38 92% 50%;
--destructive: 0 84% 70%;
--border: 217 33% 22%;
--ring: 258 90% 76%;
}
Output: Style Dictionary (JSON)¶
{
"color": {
"brand": {
"primary": {
"50": { "value": "#F5F3FF", "type": "color" },
"500": { "value": "#8B5CF6", "type": "color" },
"600": { "value": "#7C3AED", "type": "color" },
"700": { "value": "#6D28D9", "type": "color" }
}
},
"semantic": {
"primary": { "value": "{color.brand.primary.600}", "type": "color" },
"primary-hover": { "value": "{color.brand.primary.700}", "type": "color" },
"text-primary": { "value": "#0F172A", "type": "color" },
"bg-default": { "value": "#FFFFFF", "type": "color" }
}
}
}
Use guidance¶
- Reach for primary for: primary CTAs, links in body, active nav, brand emphasis. One primary CTA per surface.
- Reach for accent (amber) for: warning states, "new feature" callouts, secondary CTAs that should attract attention without competing with primary. Avoid using as a third brand color.
- Reach for neutrals for: every non-brand surface. Cards, borders, body text, dividers. The system should feel ~80% neutral.
- Do not: use
accent-600andprimary-600as adjacent buttons — they fight. If both are needed, make one ghost (transparent bg + colored text). - Do not: use
text-disabledfor live content. Disabled means "not interactable now"; if it's readable info, usetext-tertiary.
Responsive usage¶
- Mobile surfaces use
--color-bg-defaultplus one primary action; avoid stacking primary and accent CTAs inside the same 360px viewport. - Desktop dashboards may use
--color-bg-elevatedcards on--color-bg-subtle, but chart/category color usage still caps at the same palette rules. - Breakpoint changes must not change semantic token meaning:
--color-primary-defaultstays the primary action color on mobile, tablet, and desktop.
Figma variable sync¶
Figma variables mirror the semantic token layer, not the raw Tailwind ramp. Use color.primary.default, color.text.primary, and color.bg.default as the source of truth, then export/import through the token sync workflow so code and Figma stay aligned.
Cited sources¶
- knowledge/colors/color-theory.md — OKLCH rationale, ramp construction
- knowledge/colors/palettes-by-product-type.md — SaaS row used as reference shape
- knowledge/a11y/contrast.md — WCAG ratios
- Tailwind v4 OKLCH ramps (verified upstream)