Purple gradients in CSS: palettes, hex codes and blends that stay violet
Purple gradient palettes with hex codes, why violet turns pink or blue as it fades, and how to blend it in CSS.
Updated
A purple gradient runs from a pale lavender through a bright violet to a deep, inky purple. In CSS it's one linear-gradient. Add in oklab so the violet keeps its hue as it lightens, instead of turning pink:
.purple {
background: linear-gradient(to bottom, #e9e3ff, #aa8fff, #7436e3, #31156d);
background: linear-gradient(
to bottom in oklab,
#e9e3ff,
#aa8fff,
#7436e3,
#31156d
);
}The first line is the fallback for browsers that don't support in oklab. The demo shows that palette, six more from the table below, and violet #7c3aed fading to white three ways. Everything in it is plain CSS.
import type { ReactNode } from "react";
/**
* The purple recipes from the purple gradient blog post, all plain CSS. The
* first five palettes were built in OKLCH and rounded to hex; the last two
* are the colours of free presets.
*/
const palettes: readonly { name: string; colors: readonly string[] }[] = [
{ name: "Lavender", colors: ["#f1eaff", "#cdb7f6", "#9973df", "#503189"] },
{ name: "Violet", colors: ["#e9e3ff", "#aa8fff", "#7436e3", "#31156d"] },
{ name: "Grape", colors: ["#e8d5f7", "#af71e1", "#7427ad", "#300e4e"] },
{ name: "Plum", colors: ["#f3dcf4", "#c781cb", "#89348d", "#411141"] },
{ name: "Indigo", colors: ["#e0e3f9", "#888dec", "#4838bf", "#19144c"] },
{ name: "Nightfold", colors: ["#f1dcff", "#a77bff", "#3d2aa6", "#120d33"] },
{ name: "Solar Wind", colors: ["#ffe2f2", "#e879c9", "#5b309b", "#120a2a"] },
];
const blends = ["srgb", "oklab", "oklch"] as const;
const violet = palettes[1].colors.join(", ");
const label = "relative text-sm font-semibold text-white";
/**
* A box painted with `modern`, over `fallback` for browsers that drop
* gradients with an interpolation space, so no swatch ever renders empty.
*/
function Swatch({
className,
fallback,
modern,
children,
}: {
className: string;
fallback: string;
modern: string;
children: ReactNode;
}) {
return (
<div
className={`relative overflow-hidden ${className}`}
style={{ background: fallback }}
>
<div
aria-hidden
className="absolute inset-0"
style={{ background: modern }}
/>
{children}
</div>
);
}
export function PurpleGradientsDemo() {
return (
<div className="grid w-full gap-3 bg-[#040309] p-4">
<Swatch
className="flex h-48 items-end rounded-lg p-3"
fallback={`linear-gradient(to bottom, ${violet})`}
modern={`linear-gradient(to bottom in oklab, ${violet})`}
>
<span className={label}>Violet, light to dark, in oklab</span>
</Swatch>
<div className="grid gap-3 sm:grid-cols-2">
{palettes.map((palette) => (
<Swatch
key={palette.name}
className="flex h-20 items-start justify-end rounded-lg p-3"
fallback={`linear-gradient(to right, ${palette.colors.join(", ")})`}
modern={`linear-gradient(to right in oklab, ${palette.colors.join(", ")})`}
>
<span className={label}>{palette.name}</span>
</Swatch>
))}
</div>
<div className="grid gap-2">
{blends.map((space) => (
<Swatch
key={space}
className="flex h-10 items-center rounded-md px-3"
fallback="linear-gradient(to right, #7c3aed, #ffffff)"
modern={`linear-gradient(to right in ${space}, #7c3aed, #ffffff)`}
>
<span className={label}>in {space}</span>
</Swatch>
))}
</div>
</div>
);
}
Purple palettes with hex codes
Each row works as a four-stop gradient on its own, lightest to darkest. The first five were built in OKLCH, each around one hue, with lightness stepping down and chroma peaking in the middle, then rounded to hex. The last column is the hue each one measures after rounding. The last two are the colours of free presets.
| Palette | Light | Mid-light | Mid-dark | Dark | OKLCH hue |
|---|---|---|---|---|---|
| Lavender | #f1eaff | #cdb7f6 | #9973df | #503189 | 296–301° |
| Violet | #e9e3ff | #aa8fff | #7436e3 | #31156d | 290–295° |
| Grape | #e8d5f7 | #af71e1 | #7427ad | #300e4e | 304–310° |
| Plum | #f3dcf4 | #c781cb | #89348d | #411141 | 325–328° |
| Indigo | #e0e3f9 | #888dec | #4838bf | #19144c | 280° |
| Nightfold | #f1dcff | #a77bff | #3d2aa6 | #120d33 | 281–312° |
| Solar Wind | #ffe2f2 | #e879c9 | #5b309b | #120a2a | 290–343° |
"Purple" covers a wide arc of hues. Below about 285° it reads as indigo, close to blue. Violet sits around 290° to 295°. Grape and true purple are near 305°, and past 320° it turns into plum and magenta. Pick a row by where you want it to sit, then keep its hue steady. Lavender sits a few degrees warmer than Violet with less chroma, so it reads softer and greyer.
The two presets drift on purpose. Nightfold starts as a pinkish lavender at 312° and cools to indigo at 281°, like a dusk sky. Solar Wind starts in pink at 343° and sinks into violet.
Why purple is hard to blend
Violet is a colour of the spectrum, but purple isn't: no single wavelength of light looks purple. Your eye builds it from red and blue together, the two ends of the spectrum, so a blend that reaches purple from red or blue crosses a long way round the colour wheel. That is why the same gradient looks so different depending on how CSS mixes it.
Rendered in Chrome 154, here's where the halfway colour of a few common purple gradients lands, measured as OKLCH chroma (0 is grey) and hue:
| Gradient | sRGB (default) | in oklab | in oklch |
|---|---|---|---|
#7c3aed → white | #bd9cf6, hue 300° | #b7a5fb, hue 293° | #98afff, hue 271° |
red #ff0000 → blue | #7f007f, lightness 0.42 | #8c53a2, chroma 0.133 | #ba00c2, chroma 0.260 |
#7c3aed → yellow #facc15 | #bb8381, chroma 0.070 | #b293af, chroma 0.054 | #ff577a, chroma 0.203 |
#7c3aed → pink #ec4899 | #b441c3, chroma 0.212 | #b24dc5, chroma 0.197 | #bd3ccb, chroma 0.229 |
Read across a row and three rules fall out.
Fading violet to white, use in oklab. Violet #7c3aed has a hue of 293°. The default sRGB blend puts its middle at 300°, 7° toward pink, so a violet hero washes out into a pink-lilac. OKLab lands at 293°, the same violet only lighter. in oklch should match it: white has no hue, and the CSS spec says a gradient to white keeps the other colour's hue. Chrome 154 doesn't do that yet. Its halfway colour comes out at 271°, 22° toward blue. The three bars at the bottom of the demo show all three.
Making purple from red and blue, use in oklch. In sRGB, red to blue passes through #7f007f, a purple, but a dark one: its OKLCH lightness is 0.42, against 0.63 for red and 0.45 for blue. The middle of the gradient sags. OKLab keeps the lightness up but greys the colour to a dusty mauve. OKLCH walks around the colour wheel through magenta and lands on #ba00c2, the most vivid of the three by far.
Purple into yellow, use in oklch and choose the direction. Yellow is purple's opposite on the colour wheel, so a straight line between them runs through grey. sRGB gives a muddy pink-brown, #bb8381. OKLab is greyer still. OKLCH goes the short way round, through red and coral, and stays bright. Write in oklch longer hue to go the other way, through blue and green.
Purple to pink or to blue doesn't need any of this. The hues are close enough that all three spaces keep the middle coloured. #7c3aed to #ec4899 has a halfway chroma between 0.197 and 0.229 in every space, and Flood's blue #2457e6 to Nightfold's #a77bff stays between 0.199 and 0.206.
in oklab and in oklch work in Chrome and Edge 111, Safari 16.2 and Firefox 127. Older browsers drop the whole declaration, which is why the example at the top writes a plain gradient first.
Text on a purple gradient
Violet looks bright but is dark enough to carry white text. White on #7c3aed is 5.70:1, past the 4.5:1 that WCAG asks for body text. The pale end of a purple gradient is a different story: on the light and mid-light stops in the table, white manages only 1.17:1 to 3.35:1.
- White text over the darker half. White reaches 4.5:1 on every mid-dark and dark stop in the table except Lavender's
#9973df, at 3.58:1. Lavender is meant to stay soft. - Dark text over the lighter half.
#0f172a, a near-black navy, reaches at least 5.33:1 on every light and mid-light stop.
Neither works across a whole light-to-dark purple gradient. If a heading has to cross the middle, give it a dark scrim, or keep the gradient in one half of the range.
The most vivid purples are outside sRGB
Violet is where screens run out of colour early. The most vivid violets an sRGB screen can show, like #7f00ff, already have the blue channel at its ceiling and green at zero. Many of the purples designers reach for sit past that edge. Tailwind 4.3.3 defines violet-500 as oklch(60.6% 0.25 292.717), and converting it to sRGB needs a blue channel of about 106%. violet-400, purple-400 and purple-500 are out of range too.
That's not a mistake. A P3 display, which most recent phones and Macs have, shows those colours as written. An sRGB screen shows the nearest colour it can, which is a little duller. If a purple gradient has to look the same everywhere, build it from hex stops like the ones in the table, which are all inside sRGB. If you want the extra punch on displays that can show it, write the stops in oklch() and let each screen do its best.
Dark purple gradients band
Deep purples show bands, like navy does. Nightfold's darkest stop, #120d33, against its background, #040309, differs by only 14 steps of 256 in red, 10 in green and 42 in blue. As a rough estimate, a plain 8-bit gradient between those two colours across a 1440px hero changes its blue channel about once every 34 pixels, and its green channel only ten times in the whole width. Those edges can show as stripes on a dark screen.
Grain or dither breaks them up. How to add film grain to a gradient adds an SVG noise layer in plain CSS. Ordered dithering in a shader covers the fix inside a shader.
Purple gradients in Tailwind
Tailwind v4 already blends in OKLab. In Tailwind 4.3.3, bg-linear-to-b compiles to to bottom, then to bottom in oklab inside an @supports check, so a violet gradient that stays violet is one class list:
<div class="bg-linear-to-b from-violet-200 via-violet-600 to-violet-950"></div>violet-600 is oklch(54.1% 0.281 293.009), inside sRGB at about #7f22fe, and it takes white text at 5.89:1. For a red-to-blue or purple-to-yellow gradient, switch to OKLCH with bg-linear-to-r/oklch, for the reasons above. For hex stops from the table, use arbitrary values such as from-[#e9e3ff].
A purple gradient that moves
A CSS gradient holds still unless you animate it. Nightfold is a shader: violet folds of soft light drifting over a near-black ground, one of the free presets in the Soft Fold collection:
Nightfold, live
import { GradientBackground } from "@/components/gradient-surface";
import { kindDefaults, mergeParams } from "@/lib/gradient-engine";
// The free Nightfold preset: the mist shader, soft violet folds of light
// drifting over a near-black ground.
const nightfold = mergeParams(kindDefaults("mist"), {
background: "#040309",
blur: 0.7,
colors: ["#f1dcff", "#a77bff", "#3d2aa6", "#120d33"],
grain: 0.14,
interaction: { pointer: 0.2, scroll: 0.15 },
loopSeconds: 16,
scale: 0.8,
seed: 2,
speed: 0.9,
warp: 0.4,
});
export function NightfoldMistDemo() {
return (
<GradientBackground
defaults={nightfold}
className="flex h-[360px] items-start p-8"
>
<h2 className="text-3xl font-semibold tracking-tight text-white">
Nightfold, live
</h2>
</GradientBackground>
);
}
It draws with WebGPU where the browser supports it and WebGL2 otherwise, loops seamlessly every 16 seconds, adds a little grain against the banding above, and shows one still frame to visitors who prefer reduced motion.
Nightfold installs with the shadcn CLI and no account, into any React project with a components.json:
npx shadcn@latest add https://gradients.sh/r/c/eyJ2IjoxLCJwcmVzZXRJZCI6Im5pZ2h0Zm9sZCIsIm5hbWUiOiJOaWdodGZvbGQiLCJwIjp7ImtpbmQiOiJtaXN0IiwiYmFja2dyb3VuZCI6IiMwNDAzMDkiLCJibHVyIjowLjcsImNvbG9ycyI6WyIjZjFkY2ZmIiwiI2E3N2JmZiIsIiMzZDJhYTYiLCIjMTIwZDMzIl0sImxvb3BTZWNvbmRzIjoxNiwic2NhbGUiOjAuOCwic3BlZWQiOjAuOSwid2FycCI6MC40fX0.jsonimport { Nightfold } from "@/components/gradients/nightfold";
export function Hero() {
return (
<section className="relative isolate min-h-[560px]">
<Nightfold className="absolute inset-0 -z-10" />
{/* Your content sits on top. */}
</section>
);
}More free presets use violet as one colour among others: Solar Wind (pink into violet, an aurora, in the Polar Night collection), Petal Arc (violet, pink and teal), Velvet Hour (indigo, peach and lavender silk), Glasswork (blue and violet light on a pale ground) and Afterhours (pink, violet and cyan ribbons). Open any of them to tune the colours. A free account exports PNGs up to 1920px wide and 10-second MP4 loops up to 1080p60. Nightfold's loop is 16 seconds, so set its Loop to 10 in the editor before a free MP4 export, or the clip stops before the loop closes.
Related
- Blue gradients in CSS has seven blue palettes, and why blue drifts purple as it fades.
- Pastel gradients in CSS covers the pale lavenders, with the OKLCH range that makes a colour pastel.
- Sunset gradients in CSS blends orange into indigo and violet, the other hard purple pairing.
- Gradients that follow light and dark mode switches a palette like these with CSS variables.
- Green gradients in CSS has eight green palettes, and why green into purple runs through grey-blue.
- Red gradients in CSS has eight red palettes, and why red and blue meet in magenta only in OKLCH.