Tailwind shade scale generator
Turn one base colour into a 50 to 950 shade scale by mixing toward white and black in HSL, ready for Tailwind theme tokens.
React / Next.jsThemingtailwindcolorpalettehsl
Code
export const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const;
// Target lightness (%) for each step of the ramp.
const LIGHTNESS = [97, 93, 85, 75, 63, 52, 43, 35, 27, 19, 11];
type HSL = [number, number, number]; // h 0-360, s/l 0-100
function hexToHsl(hex: string): HSL {
const v = hex.replace('#', '');
const full = v.length === 3 ? [...v].map((c) => c + c).join('') : v;
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255) as [number, number, number];
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
const d = max - min;
if (d === 0) return [0, 0, l * 100];
const s = d / (1 - Math.abs(2 * l - 1));
const h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
return [(h * 60 + 360) % 360, s * 100, l * 100];
}
function hslToHex([h, s, l]: HSL): string {
const sat = s / 100;
const lig = l / 100;
const a = sat * Math.min(lig, 1 - lig);
const f = (n: number) => {
const k = (n + h / 30) % 12;
const c = lig - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
return Math.round(c * 255).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
}
export function shades(base: string): Record<number, string> {
const [h, s, l] = hexToHsl(base);
// The step whose target lightness is closest to the input keeps the exact input colour.
const anchor = LIGHTNESS.reduce((best, v, i) => (Math.abs(v - l) < Math.abs(LIGHTNESS[best]! - l) ? i : best), 0);
const out: Record<number, string> = {};
STEPS.forEach((step, i) => {
if (i === anchor) return void (out[step] = hslToHex([h, s, l]));
const edge = Math.abs(LIGHTNESS[i]! - 50) / 50; // 0 in the middle, ~1 at the ends
const sat = s === 0 ? 0 : s * (1 - edge * 0.25); // ease saturation at the extremes
out[step] = hslToHex([h, sat, LIGHTNESS[i]!]);
});
return out;
}
export const toCssVars = (name: string, scale: Record<number, string>) =>
STEPS.map((step) => `--color-${name}-${step}: ${scale[step]};`).join('\n');
How it works
What it solves
Design systems need a full ramp of lighter and darker tints from a single brand colour: backgrounds at 50, borders at 200, text at 900. Building that by hand is slow and inconsistent. This function generates the eleven steps used by Tailwind from any hex value and prints them as CSS variables.
How it works
The colour is converted from hex to HSL. Hue stays constant so every shade reads as the same colour family. The scale is then defined by a table of target lightness values, from about 97 percent for step 50 down to 10 percent for 950, anchored so the step closest to the input lightness reproduces the original colour exactly. That anchoring means the brand colour appears in the scale unchanged rather than being approximated.
Saturation is eased: very light and very dark steps lose a little saturation, which avoids neon pastels and muddy darks. The helper converts each HSL value back to hex with the standard chroma formula.
toCssVars returns lines such as --color-brand-500: #3366cc;, the naming Tailwind 4 reads from an @theme block, so the output can be pasted straight into a stylesheet.
Adapting it
- Replace the lightness table with your own curve for flatter or punchier ramps.
- Convert to OKLCH for perceptually even steps; the structure is identical.
- Add contrast checks to flag steps that fail against white.
Pitfalls
- HSL lightness is not perceptual, so yellow ramps look brighter than blue ones at the same value.
- Greys with zero saturation ignore hue; the code handles that by skipping the saturation easing.
How to use it
- Save as
lib/shades.ts. const scale = shades('#3b82f6')returns an object keyed 50 to 950.toCssVars('brand', scale)returns lines to paste into an@themeblock.- Render the object as swatches for a live editor.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.