콘텐츠로 이동

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-palette skill

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-600 and primary-600 as adjacent buttons — they fight. If both are needed, make one ghost (transparent bg + colored text).
  • Do not: use text-disabled for live content. Disabled means "not interactable now"; if it's readable info, use text-tertiary.

Responsive usage

  • Mobile surfaces use --color-bg-default plus one primary action; avoid stacking primary and accent CTAs inside the same 360px viewport.
  • Desktop dashboards may use --color-bg-elevated cards 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-default stays 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