Your tailwind.config.js has brand: '#6D39AC'. That works until you need a hover state, a disabled border, a subtle table stripe, and accessible link text. Now you are hand-picking five related purples in a spreadsheet, and they do not match each other.
Tailwind's numbered scale (50 through 950) exists because UI needs more than one swatch. Here is how to generate that ramp from a single brand HEX without muddy tints or contrast surprises.
What is a Tailwind color scale?
Tailwind maps semantic utility classes to numbered steps: bg-brand-500, text-brand-700, border-brand-200. The numbers are not arbitrary. They imply lightness progression.
- 50–200: backgrounds, subtle borders, table stripes
- 300–400: hover fills, secondary buttons
- 500–600: primary actions, default links
- 700–950: headings, emphasis, dark mode surfaces
If your design system only documents #6D39AC, engineers guess. Designers pick one-off HEX values. The product slowly stops looking like one brand.
Why hand-mixing tints and shades goes wrong
Lightening a color in HSL can shift hue (a blue tint turns cyan). Mixing in Photoshop without a system produces steps that look uneven on different monitors.
Linear RGB mixing toward white and black is predictable: each step is mathematically consistent. That is what most scale generators use, including the tint and shade tool on Color Mapper.
The trade-off: linear ramps are not always the most beautiful ramps. Some design teams tweak 400 and 600 by eye after generation. Starting from a generated base saves hours.
How to build a 50–950 scale
Tailwind's default palettes use eleven steps, 50 through 950. The customizing-colors guide is where you add your own under theme.extend.colors without replacing the defaults. Sources: Tailwind colors and customizing colors (v3). The color scale generator builds that shape from one hex. Here is the sequence, including what to do if you are laying the ramp out by hand.
- Lock the seed as step 500. The color people already recognize — the logo, the primary button — is 500. Do not let a mix replace it. For the worked example below, the seed is
#6D39AC. - Mix toward white in sRGB for 50–400. Take the red, green, and blue channels and interpolate them toward 255. Do this in sRGB, not by dragging HSL lightness. Lightness-only edits are how a blue tint turns cyan. The generator does the channel mix for you. Paste the seed into the color scale tool and read the lighter swatches instead of inventing them.
- Mix toward black for 600–950. Same idea, toward 0. Leave a gap between steps that your eye can see at a glance. If 700 and 800 look identical on your laptop, the ramp is too compressed to be useful as borders versus text.
- Read contrast on white and on black for every step. The badges use the WCAG relative-luminance ratio from Success Criterion 1.4.3. For
#6D39ACthe linear mixes come out like this (ratios against white, then black):
| Step | HEX | On white | On black |
|---|---|---|---|
| 50 | #F8F5FB | 1.08:1 | 19.45:1 |
| 100 | #E9E1F3 | 1.27:1 | 16.53:1 |
| 200 | #D3C4E6 | 1.64:1 | 12.81:1 |
| 300 | #BDA6DA | 2.18:1 | 9.63:1 |
| 400 | #A788CD | 2.98:1 | 7.05:1 |
| 500 | #6D39AC | 7.42:1 | 2.83:1 |
| 600 | #5D3092 | 9.12:1 | 2.30:1 |
| 700 | #4C2878 | 11.16:1 | 1.88:1 |
| 800 | #3C1F5F | 13.52:1 | 1.55:1 |
| 900 | #2C1745 | 15.96:1 | 1.32:1 |
| 950 | #0D0715 | 19.84:1 | 1.06:1 |
Step 400 at 2.98:1 on white fails even the 3:1 large-text bar. Step 500 passes AAA on white and fails on black. Those two facts are why the ramp exists: one hex cannot be both the pale surface and the text.
- Assign roles before anyone uses the numbers. 50–200 for surfaces and stripes. 600–900 for text on white. 500 for fills where white text also clears 4.5:1 — here white on
#6D39ACis the same 7.42:1, so it does. Write the rule down. "Primary" is not a step number until you say which step. - Export one key per step. Copy the Tailwind config or the CSS variables from the generator, then make sure each of 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, and 950 appears once. A duplicated key is invalid as a design token even if a JS object happens to keep the last one.
// tailwind.config.js — theme.extend.colors
// sRGB mixes of #6D39AC, 500 held at the seed
brand: {
50: '#F8F5FB',
100: '#E9E1F3',
200: '#D3C4E6',
300: '#BDA6DA',
400: '#A788CD',
500: '#6D39AC',
600: '#5D3092',
700: '#4C2878',
800: '#3C1F5F',
900: '#2C1745',
950: '#0D0715',
},
Paste, run the build, and use bg-brand-500 and text-brand-800 in components. No ad-hoc hex in JSX.
- Stop, then decide if the ramp needs a perceptual pass. Linear sRGB is predictable and it is not perfectly even to the eye. If 200 and 300 feel too far apart, adjust those two steps. Björn Ottosson's Oklab write-up is the reference for a space built to be more even. Do that after the contrast table is green for the pairs you ship, not before. A beautiful OKLCH curve that fails 4.5:1 is still a failed text color.
Which steps to use for text vs fills
Do not assume 500 works for both button background and body text on white.
| Combination | Typical result | What to do |
|---|---|---|
text-brand-500 on white | Often fails WCAG | Use 700 or 800 for text |
bg-brand-500 with white text | Usually passes | Verify with contrast checker |
text-brand-400 on bg-brand-50 | Frequently fails | Matrix-check in contrast checker |
Run non-standard pairs through the checker if your design system combines steps creatively (e.g. tinted cards with mid-tone labels).
CMYK columns for brand guidelines
Each step in the scale generator includes CMYK values. Useful when the same brand ships web and print.
Steps at 400–500 often trigger gamut warnings on neon seeds. Mark those as digital-first in your PDF brand guide. Use 700+ for print-adjacent dark text specs.
Connecting scales to palettes and gradients
A common pipeline:
- Explore harmonies in the palette generator.
- Promote one swatch to the scale generator for the full ramp.
- Feed 200/500/800 into the gradient generator for marketing heroes.
- Matrix-check text pairs in the contrast checker.
One seed HEX becomes tokens for engineering, specs for print, and stops for CSS gradients. You are not rebuilding the ramp in three tools.
What this approach does not solve
- Perceptually uniform scales (OKLCH). Tailwind 50–950 in linear RGB is good enough for most products. High-end brand systems may refine in OKLCH afterward.
- Dark mode as inverted light mode. You may need a separate dark ramp, not a reuse of the same numbers.
- Automatic semantic naming. The generator outputs numbered steps. Your team still maps
brand-600→ "primary button" in docs.
FAQ
Why 11 steps from 50 to 950?
Tailwind's default palettes use 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, and 950. Matching that list keeps a custom color pasteable into theme.extend.colors, as described in the v3 customizing-colors guide.
Should I build the ramp in HSL or in sRGB?
Mix the sRGB channels toward white and black. Changing only HSL lightness often shifts hue, so a blue tint drifts toward cyan. The color scale generator does the channel mix. Check the result by eye after it.
Should 500 always be the input HEX?
Yes if that hex is the color you need to reproduce exactly. If the Figma "primary" is a light tint, do not pretend it is a mid step. Put the real brand color on 500 and let the lighter mixes occupy 50–400.
Can I use step 400 as text on white?
Not in the #6D39AC example. Step 400, #A788CD, is 2.98:1 on white, which is under the 3:1 large-text minimum and under the 4.5:1 body-text minimum in WCAG 2.2.
Do I need both a Tailwind export and CSS variables?
Use the Tailwind export for a Tailwind project. CSS variables fit vanilla CSS, other frameworks, or a token pipeline. The hex values should match either way.
Is a linear ramp the same as an OKLCH ramp?
No. Linear sRGB mixes are even in channel values, not in perception. Oklab is the reference for a more even space. Either ramp still has to pass contrast for the pairs you ship.
Open the color scale generator, paste your production primary, and export the config. Your next PR can replace scattered hex literals with brand-* utilities in one pass.