07 / 20
All 20 in 20 CSS Grain and Noise Textures
- 01SVG feTurbulence Noise Parameter Breakdown
- 02baseFrequency and numOctaves Grain Size Control
- 03Data URI SVG Noise Without an Image Request
- 04Grain Opacity and mix-blend-mode Scale
- 05Tinting SVG Noise with feColorMatrix
- 06Grain Paint Cost and Byte Size Measured
- 07Anti-Banding Grainy CSS Gradient
- 08Vignette Photo Overlay with Film Grain
- 09Tactile Card Surface with Subtle Noise
- 10Full-Bleed Hero Section Noise Background
- 11Noise Layer Order Over backdrop-filter
- 12Textured Button and Input Surfaces
- 13Textured Section Divider with SVG Grain
- 14Static Halftone Dot Print on a Card
- 15Dithered Bayer Matrix Shading Effect
- 16Paper Stock and Letterpress Press Effect
- 17Risograph Print Misregistration Texture
- 18Woven Fabric and Canvas Texture Surface
- 19Dark Mode Noise Without Muddy Shadows
- 20Reduced Motion Static Grain Fallback
Anti-Banding Grainy CSS Gradient
Banding is not a bug in your gradient, it is the 8-bit display pipeline showing through. The fix is the same one film and print have used for a century: add noise. This is a full-bleed dusk hero built the correct way — oklab interpolation, a grain dither layer, and an honest side-by-side against the untreated version.
Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time
Untitled UI · Affiliate link
The code
<section class="grn-07" aria-labelledby="grn-07-title">
<div class="grn-07__theme">
<input class="grn-07__theme-input" type="checkbox" id="grn-07-theme">
<label class="grn-07__theme-label" for="grn-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="grn-07__sr">Toggle dark theme</span>
</label>
</div>
<header class="grn-07__hero">
<div class="grn-07__hero-copy">
<p class="grn-07__kicker">Dusk · 21:04 · clear</p>
<h1 class="grn-07__h1" id="grn-07-title">A gradient with nowhere to hide</h1>
<p class="grn-07__lede">Deep blue to violet across a full viewport is the hardest banding case on the web. This one is interpolated in <code>oklab</code>, stopped three times through the dark third, and dithered with grain at 3.5%.</p>
<dl class="grn-07__specs">
<div><dt>Interpolation</dt><dd>in oklab</dd></div>
<div><dt>Dither</dt><dd>0.035</dd></div>
<div><dt>Stops</dt><dd>6</dd></div>
</dl>
</div>
</header>
<main class="grn-07__main">
<section class="grn-07__ab" aria-labelledby="grn-07-h-ab">
<div class="grn-07__ab-head">
<h2 class="grn-07__h2" id="grn-07-h-ab">The same ramp, treated and untreated</h2>
<p class="grn-07__note">If you cannot see the difference, turn your screen brightness down — banding lives in the shadows.</p>
</div>
<div class="grn-07__ab-grid">
<figure class="grn-07__panel grn-07__panel--raw">
<div class="grn-07__field"></div>
<figcaption><b>Untreated</b><span>sRGB interpolation, two stops, no dither</span></figcaption>
</figure>
<figure class="grn-07__panel grn-07__panel--fixed">
<div class="grn-07__field"></div>
<figcaption><b>Treated</b><span>oklab, six stops, 3.5% grain dither</span></figcaption>
</figure>
</div>
</section>
<section class="grn-07__steps" aria-labelledby="grn-07-h-steps">
<h2 class="grn-07__h2" id="grn-07-h-steps">Ship it in this order</h2>
<ol class="grn-07__list">
<li><b>Interpolate in oklab.</b> Perceptually even steps mean smaller bands before you dither anything.</li>
<li><b>Add stops where it bands.</b> Almost always the darkest third. Two extra stops halve the step size.</li>
<li><b>Dither with grain at 0.03.</b> Raise in 0.005 increments until the bands go. Stop there.</li>
<li><b>Check on an OLED phone at low brightness.</b> The harshest banding test that exists, and the one nobody runs.</li>
</ol>
</section>
</main>
</section><section class="grn-07" aria-labelledby="grn-07-title">
<div class="grn-07__theme">
<input class="grn-07__theme-input" type="checkbox" id="grn-07-theme">
<label class="grn-07__theme-label" for="grn-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="grn-07__sr">Toggle dark theme</span>
</label>
</div>
<header class="grn-07__hero">
<div class="grn-07__hero-copy">
<p class="grn-07__kicker">Dusk · 21:04 · clear</p>
<h1 class="grn-07__h1" id="grn-07-title">A gradient with nowhere to hide</h1>
<p class="grn-07__lede">Deep blue to violet across a full viewport is the hardest banding case on the web. This one is interpolated in <code>oklab</code>, stopped three times through the dark third, and dithered with grain at 3.5%.</p>
<dl class="grn-07__specs">
<div><dt>Interpolation</dt><dd>in oklab</dd></div>
<div><dt>Dither</dt><dd>0.035</dd></div>
<div><dt>Stops</dt><dd>6</dd></div>
</dl>
</div>
</header>
<main class="grn-07__main">
<section class="grn-07__ab" aria-labelledby="grn-07-h-ab">
<div class="grn-07__ab-head">
<h2 class="grn-07__h2" id="grn-07-h-ab">The same ramp, treated and untreated</h2>
<p class="grn-07__note">If you cannot see the difference, turn your screen brightness down — banding lives in the shadows.</p>
</div>
<div class="grn-07__ab-grid">
<figure class="grn-07__panel grn-07__panel--raw">
<div class="grn-07__field"></div>
<figcaption><b>Untreated</b><span>sRGB interpolation, two stops, no dither</span></figcaption>
</figure>
<figure class="grn-07__panel grn-07__panel--fixed">
<div class="grn-07__field"></div>
<figcaption><b>Treated</b><span>oklab, six stops, 3.5% grain dither</span></figcaption>
</figure>
</div>
</section>
<section class="grn-07__steps" aria-labelledby="grn-07-h-steps">
<h2 class="grn-07__h2" id="grn-07-h-steps">Ship it in this order</h2>
<ol class="grn-07__list">
<li><b>Interpolate in oklab.</b> Perceptually even steps mean smaller bands before you dither anything.</li>
<li><b>Add stops where it bands.</b> Almost always the darkest third. Two extra stops halve the step size.</li>
<li><b>Dither with grain at 0.03.</b> Raise in 0.005 increments until the bands go. Stop there.</li>
<li><b>Check on an OLED phone at low brightness.</b> The harshest banding test that exists, and the one nobody runs.</li>
</ol>
</section>
</main>
</section>.grn-07 {
--grn-07-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='grn07n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23grn07n)'/%3E%3C/svg%3E");
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-dither: 0.035;
--grn-07-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
--grn-07-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--grn-07-bg);
color: var(--grn-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.grn-07 *,
.grn-07 *::before,
.grn-07 *::after {
box-sizing: border-box;
}
.grn-07 :focus-visible {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07 code {
font-family: var(--grn-07-font-mono);
font-size: 0.9em;
}
.grn-07__sr {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
@media (prefers-color-scheme: light) {
.grn-07 {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-dither: 0.035;
}
}
@media (prefers-color-scheme: dark) {
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
}
.grn-07__theme {
position: absolute;
inset-block-start: clamp(1rem, 3vw, 1.75rem);
inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
z-index: 40;
}
.grn-07__theme-input {
position: absolute;
inset: 0;
opacity: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
cursor: pointer;
}
.grn-07__theme-label {
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 999px;
background: rgba(10, 8, 20, 0.4);
color: #f2eefb;
cursor: pointer;
backdrop-filter: blur(6px);
}
.grn-07__theme-label svg {
inline-size: 20px;
block-size: 20px;
}
.grn-07__theme-input:focus-visible + .grn-07__theme-label {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07__hero {
position: relative;
isolation: isolate;
overflow: hidden;
min-block-size: min(86svh, 46rem);
display: grid;
align-items: end;
background-image: radial-gradient(120% 90% at 78% 6%, color-mix(in oklab, var(--grn-07-c4) 55%, transparent) 0%, transparent 58%), linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 14%, var(--grn-07-c2) 38%, var(--grn-07-c2) 48%, var(--grn-07-c3) 74%, var(--grn-07-c4) 100%);
}
.grn-07__hero::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__hero-copy {
position: relative;
z-index: 2;
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(4rem, 12vw, 7rem);
display: grid;
gap: 0.85rem;
max-inline-size: 44rem;
}
.grn-07__kicker {
margin: 0;
font: 500 0.74rem/1 var(--grn-07-font-mono);
letter-spacing: 0.18em;
text-transform: uppercase;
color: color-mix(in oklab, var(--grn-07-ink) 78%, transparent);
}
.grn-07__h1 {
margin: 0;
font: 600 clamp(2.3rem, 7vw, 4.6rem)/0.99 var(--grn-07-font-display);
letter-spacing: -0.04em;
text-wrap: balance;
}
.grn-07__lede {
margin: 0;
max-inline-size: 34rem;
color: color-mix(in oklab, var(--grn-07-ink) 82%, transparent);
font-size: clamp(0.97rem, 2vw, 1.14rem);
text-wrap: pretty;
}
.grn-07__specs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0 0;
}
.grn-07__specs > div {
display: grid;
gap: 0.1rem;
padding: 0.55rem 0.85rem;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 10px;
background: rgba(8, 6, 15, 0.3);
backdrop-filter: blur(4px);
}
.grn-07__specs dt {
margin: 0;
font: 400 0.68rem/1 var(--grn-07-font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
}
.grn-07__specs dd {
margin: 0;
font: 500 0.88rem/1.2 var(--grn-07-font-mono);
}
.grn-07__main {
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(2.5rem, 7vw, 4.5rem);
display: grid;
gap: clamp(2rem, 5vw, 3rem);
}
.grn-07__ab {
display: grid;
gap: 1rem;
}
.grn-07__ab-head {
display: grid;
gap: 0.35rem;
padding-block-end: 0.7rem;
border-block-end: 1px solid var(--grn-07-line);
}
.grn-07__h2 {
margin: 0;
font: 600 clamp(1.15rem, 2.8vw, 1.5rem)/1.2 var(--grn-07-font-display);
letter-spacing: -0.02em;
}
.grn-07__note {
margin: 0;
color: var(--grn-07-mut);
font-size: 0.88rem;
text-wrap: pretty;
}
.grn-07__ab-grid {
display: grid;
gap: clamp(0.8rem, 2vw, 1.3rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}
.grn-07__panel {
margin: 0;
display: grid;
gap: 0.6rem;
min-inline-size: 0;
}
.grn-07__field {
position: relative;
isolation: isolate;
overflow: hidden;
block-size: clamp(11rem, 26vw, 17rem);
border-radius: 14px;
}
.grn-07__panel--raw .grn-07__field {
background-image: linear-gradient(168deg, var(--grn-07-c1), var(--grn-07-c3));
}
.grn-07__panel--fixed .grn-07__field {
background-image: linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 12%, var(--grn-07-c2) 34%, var(--grn-07-c2) 46%, var(--grn-07-c3) 78%, var(--grn-07-c3) 100%);
}
.grn-07__panel--fixed .grn-07__field::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__panel figcaption {
display: grid;
gap: 0.15rem;
}
.grn-07__panel figcaption b {
font: 600 0.95rem/1.2 var(--grn-07-font-display);
}
.grn-07__panel figcaption span {
color: var(--grn-07-mut);
font: 400 0.8rem/1.4 var(--grn-07-font-mono);
}
.grn-07__steps {
display: grid;
gap: 0.9rem;
padding: clamp(1.2rem, 3vw, 1.9rem);
border: 1px solid var(--grn-07-line);
border-radius: 16px;
background: var(--grn-07-surf);
}
.grn-07__list {
margin: 0;
padding-inline-start: 1.3rem;
display: grid;
gap: 0.55rem;
color: var(--grn-07-mut);
font-size: 0.92rem;
}
.grn-07__list b {
color: var(--grn-07-ink);
font-weight: 600;
}
.grn-07__list::marker {
color: var(--grn-07-c4);
}
@media (prefers-reduced-motion: reduce) {
.grn-07 * {
animation-duration: 1ms !important;
transition-duration: 1ms !important;
}
}.grn-07 {
--grn-07-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='grn07n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23grn07n)'/%3E%3C/svg%3E");
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-dither: 0.035;
--grn-07-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
--grn-07-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--grn-07-bg);
color: var(--grn-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.grn-07 *,
.grn-07 *::before,
.grn-07 *::after {
box-sizing: border-box;
}
.grn-07 :focus-visible {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07 code {
font-family: var(--grn-07-font-mono);
font-size: 0.9em;
}
.grn-07__sr {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
@media (prefers-color-scheme: light) {
.grn-07 {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-dither: 0.035;
}
}
@media (prefers-color-scheme: dark) {
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
}
.grn-07__theme {
position: absolute;
inset-block-start: clamp(1rem, 3vw, 1.75rem);
inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
z-index: 40;
}
.grn-07__theme-input {
position: absolute;
inset: 0;
opacity: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
cursor: pointer;
}
.grn-07__theme-label {
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 999px;
background: rgba(10, 8, 20, 0.4);
color: #f2eefb;
cursor: pointer;
backdrop-filter: blur(6px);
}
.grn-07__theme-label svg {
inline-size: 20px;
block-size: 20px;
}
.grn-07__theme-input:focus-visible + .grn-07__theme-label {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07__hero {
position: relative;
isolation: isolate;
overflow: hidden;
min-block-size: min(86svh, 46rem);
display: grid;
align-items: end;
background-image: radial-gradient(120% 90% at 78% 6%, color-mix(in oklab, var(--grn-07-c4) 55%, transparent) 0%, transparent 58%), linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 14%, var(--grn-07-c2) 38%, var(--grn-07-c2) 48%, var(--grn-07-c3) 74%, var(--grn-07-c4) 100%);
}
.grn-07__hero::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__hero-copy {
position: relative;
z-index: 2;
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(4rem, 12vw, 7rem);
display: grid;
gap: 0.85rem;
max-inline-size: 44rem;
}
.grn-07__kicker {
margin: 0;
font: 500 0.74rem/1 var(--grn-07-font-mono);
letter-spacing: 0.18em;
text-transform: uppercase;
color: color-mix(in oklab, var(--grn-07-ink) 78%, transparent);
}
.grn-07__h1 {
margin: 0;
font: 600 clamp(2.3rem, 7vw, 4.6rem)/0.99 var(--grn-07-font-display);
letter-spacing: -0.04em;
text-wrap: balance;
}
.grn-07__lede {
margin: 0;
max-inline-size: 34rem;
color: color-mix(in oklab, var(--grn-07-ink) 82%, transparent);
font-size: clamp(0.97rem, 2vw, 1.14rem);
text-wrap: pretty;
}
.grn-07__specs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0 0;
}
.grn-07__specs > div {
display: grid;
gap: 0.1rem;
padding: 0.55rem 0.85rem;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 10px;
background: rgba(8, 6, 15, 0.3);
backdrop-filter: blur(4px);
}
.grn-07__specs dt {
margin: 0;
font: 400 0.68rem/1 var(--grn-07-font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
}
.grn-07__specs dd {
margin: 0;
font: 500 0.88rem/1.2 var(--grn-07-font-mono);
}
.grn-07__main {
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(2.5rem, 7vw, 4.5rem);
display: grid;
gap: clamp(2rem, 5vw, 3rem);
}
.grn-07__ab {
display: grid;
gap: 1rem;
}
.grn-07__ab-head {
display: grid;
gap: 0.35rem;
padding-block-end: 0.7rem;
border-block-end: 1px solid var(--grn-07-line);
}
.grn-07__h2 {
margin: 0;
font: 600 clamp(1.15rem, 2.8vw, 1.5rem)/1.2 var(--grn-07-font-display);
letter-spacing: -0.02em;
}
.grn-07__note {
margin: 0;
color: var(--grn-07-mut);
font-size: 0.88rem;
text-wrap: pretty;
}
.grn-07__ab-grid {
display: grid;
gap: clamp(0.8rem, 2vw, 1.3rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}
.grn-07__panel {
margin: 0;
display: grid;
gap: 0.6rem;
min-inline-size: 0;
}
.grn-07__field {
position: relative;
isolation: isolate;
overflow: hidden;
block-size: clamp(11rem, 26vw, 17rem);
border-radius: 14px;
}
.grn-07__panel--raw .grn-07__field {
background-image: linear-gradient(168deg, var(--grn-07-c1), var(--grn-07-c3));
}
.grn-07__panel--fixed .grn-07__field {
background-image: linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 12%, var(--grn-07-c2) 34%, var(--grn-07-c2) 46%, var(--grn-07-c3) 78%, var(--grn-07-c3) 100%);
}
.grn-07__panel--fixed .grn-07__field::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__panel figcaption {
display: grid;
gap: 0.15rem;
}
.grn-07__panel figcaption b {
font: 600 0.95rem/1.2 var(--grn-07-font-display);
}
.grn-07__panel figcaption span {
color: var(--grn-07-mut);
font: 400 0.8rem/1.4 var(--grn-07-font-mono);
}
.grn-07__steps {
display: grid;
gap: 0.9rem;
padding: clamp(1.2rem, 3vw, 1.9rem);
border: 1px solid var(--grn-07-line);
border-radius: 16px;
background: var(--grn-07-surf);
}
.grn-07__list {
margin: 0;
padding-inline-start: 1.3rem;
display: grid;
gap: 0.55rem;
color: var(--grn-07-mut);
font-size: 0.92rem;
}
.grn-07__list b {
color: var(--grn-07-ink);
font-weight: 600;
}
.grn-07__list::marker {
color: var(--grn-07-c4);
}
@media (prefers-reduced-motion: reduce) {
.grn-07 * {
animation-duration: 1ms !important;
transition-duration: 1ms !important;
}
}Here's a working CSS Grain and Noise Texture from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Anti-Banding Grainy CSS Gradient
Source: https://codefronts.com/design-styles/css-grain-texture/anti-banding-grainy-css-gradient/
Banding is not a bug in your gradient, it is the 8-bit display pipeline showing through. The fix is the same one film and print have used for a century: add noise. This is a full-bleed dusk hero built the correct way — oklab interpolation, a grain dither layer, and an honest side-by-side against the untreated version.
## HTML
```html
<section class="grn-07" aria-labelledby="grn-07-title">
<div class="grn-07__theme">
<input class="grn-07__theme-input" type="checkbox" id="grn-07-theme">
<label class="grn-07__theme-label" for="grn-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="grn-07__sr">Toggle dark theme</span>
</label>
</div>
<header class="grn-07__hero">
<div class="grn-07__hero-copy">
<p class="grn-07__kicker">Dusk · 21:04 · clear</p>
<h1 class="grn-07__h1" id="grn-07-title">A gradient with nowhere to hide</h1>
<p class="grn-07__lede">Deep blue to violet across a full viewport is the hardest banding case on the web. This one is interpolated in <code>oklab</code>, stopped three times through the dark third, and dithered with grain at 3.5%.</p>
<dl class="grn-07__specs">
<div><dt>Interpolation</dt><dd>in oklab</dd></div>
<div><dt>Dither</dt><dd>0.035</dd></div>
<div><dt>Stops</dt><dd>6</dd></div>
</dl>
</div>
</header>
<main class="grn-07__main">
<section class="grn-07__ab" aria-labelledby="grn-07-h-ab">
<div class="grn-07__ab-head">
<h2 class="grn-07__h2" id="grn-07-h-ab">The same ramp, treated and untreated</h2>
<p class="grn-07__note">If you cannot see the difference, turn your screen brightness down — banding lives in the shadows.</p>
</div>
<div class="grn-07__ab-grid">
<figure class="grn-07__panel grn-07__panel--raw">
<div class="grn-07__field"></div>
<figcaption><b>Untreated</b><span>sRGB interpolation, two stops, no dither</span></figcaption>
</figure>
<figure class="grn-07__panel grn-07__panel--fixed">
<div class="grn-07__field"></div>
<figcaption><b>Treated</b><span>oklab, six stops, 3.5% grain dither</span></figcaption>
</figure>
</div>
</section>
<section class="grn-07__steps" aria-labelledby="grn-07-h-steps">
<h2 class="grn-07__h2" id="grn-07-h-steps">Ship it in this order</h2>
<ol class="grn-07__list">
<li><b>Interpolate in oklab.</b> Perceptually even steps mean smaller bands before you dither anything.</li>
<li><b>Add stops where it bands.</b> Almost always the darkest third. Two extra stops halve the step size.</li>
<li><b>Dither with grain at 0.03.</b> Raise in 0.005 increments until the bands go. Stop there.</li>
<li><b>Check on an OLED phone at low brightness.</b> The harshest banding test that exists, and the one nobody runs.</li>
</ol>
</section>
</main>
</section>
```
## CSS
```css
.grn-07 {
--grn-07-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='grn07n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23grn07n)'/%3E%3C/svg%3E");
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-dither: 0.035;
--grn-07-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
--grn-07-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--grn-07-bg);
color: var(--grn-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.grn-07 *,
.grn-07 *::before,
.grn-07 *::after {
box-sizing: border-box;
}
.grn-07 :focus-visible {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07 code {
font-family: var(--grn-07-font-mono);
font-size: 0.9em;
}
.grn-07__sr {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
@media (prefers-color-scheme: light) {
.grn-07 {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-dither: 0.035;
}
}
@media (prefers-color-scheme: dark) {
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
}
.grn-07__theme {
position: absolute;
inset-block-start: clamp(1rem, 3vw, 1.75rem);
inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
z-index: 40;
}
.grn-07__theme-input {
position: absolute;
inset: 0;
opacity: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
cursor: pointer;
}
.grn-07__theme-label {
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 999px;
background: rgba(10, 8, 20, 0.4);
color: #f2eefb;
cursor: pointer;
backdrop-filter: blur(6px);
}
.grn-07__theme-label svg {
inline-size: 20px;
block-size: 20px;
}
.grn-07__theme-input:focus-visible + .grn-07__theme-label {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07__hero {
position: relative;
isolation: isolate;
overflow: hidden;
min-block-size: min(86svh, 46rem);
display: grid;
align-items: end;
background-image: radial-gradient(120% 90% at 78% 6%, color-mix(in oklab, var(--grn-07-c4) 55%, transparent) 0%, transparent 58%), linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 14%, var(--grn-07-c2) 38%, var(--grn-07-c2) 48%, var(--grn-07-c3) 74%, var(--grn-07-c4) 100%);
}
.grn-07__hero::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__hero-copy {
position: relative;
z-index: 2;
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(4rem, 12vw, 7rem);
display: grid;
gap: 0.85rem;
max-inline-size: 44rem;
}
.grn-07__kicker {
margin: 0;
font: 500 0.74rem/1 var(--grn-07-font-mono);
letter-spacing: 0.18em;
text-transform: uppercase;
color: color-mix(in oklab, var(--grn-07-ink) 78%, transparent);
}
.grn-07__h1 {
margin: 0;
font: 600 clamp(2.3rem, 7vw, 4.6rem)/0.99 var(--grn-07-font-display);
letter-spacing: -0.04em;
text-wrap: balance;
}
.grn-07__lede {
margin: 0;
max-inline-size: 34rem;
color: color-mix(in oklab, var(--grn-07-ink) 82%, transparent);
font-size: clamp(0.97rem, 2vw, 1.14rem);
text-wrap: pretty;
}
.grn-07__specs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0 0;
}
.grn-07__specs > div {
display: grid;
gap: 0.1rem;
padding: 0.55rem 0.85rem;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 10px;
background: rgba(8, 6, 15, 0.3);
backdrop-filter: blur(4px);
}
.grn-07__specs dt {
margin: 0;
font: 400 0.68rem/1 var(--grn-07-font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
}
.grn-07__specs dd {
margin: 0;
font: 500 0.88rem/1.2 var(--grn-07-font-mono);
}
.grn-07__main {
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(2.5rem, 7vw, 4.5rem);
display: grid;
gap: clamp(2rem, 5vw, 3rem);
}
.grn-07__ab {
display: grid;
gap: 1rem;
}
.grn-07__ab-head {
display: grid;
gap: 0.35rem;
padding-block-end: 0.7rem;
border-block-end: 1px solid var(--grn-07-line);
}
.grn-07__h2 {
margin: 0;
font: 600 clamp(1.15rem, 2.8vw, 1.5rem)/1.2 var(--grn-07-font-display);
letter-spacing: -0.02em;
}
.grn-07__note {
margin: 0;
color: var(--grn-07-mut);
font-size: 0.88rem;
text-wrap: pretty;
}
.grn-07__ab-grid {
display: grid;
gap: clamp(0.8rem, 2vw, 1.3rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}
.grn-07__panel {
margin: 0;
display: grid;
gap: 0.6rem;
min-inline-size: 0;
}
.grn-07__field {
position: relative;
isolation: isolate;
overflow: hidden;
block-size: clamp(11rem, 26vw, 17rem);
border-radius: 14px;
}
.grn-07__panel--raw .grn-07__field {
background-image: linear-gradient(168deg, var(--grn-07-c1), var(--grn-07-c3));
}
.grn-07__panel--fixed .grn-07__field {
background-image: linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 12%, var(--grn-07-c2) 34%, var(--grn-07-c2) 46%, var(--grn-07-c3) 78%, var(--grn-07-c3) 100%);
}
.grn-07__panel--fixed .grn-07__field::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__panel figcaption {
display: grid;
gap: 0.15rem;
}
.grn-07__panel figcaption b {
font: 600 0.95rem/1.2 var(--grn-07-font-display);
}
.grn-07__panel figcaption span {
color: var(--grn-07-mut);
font: 400 0.8rem/1.4 var(--grn-07-font-mono);
}
.grn-07__steps {
display: grid;
gap: 0.9rem;
padding: clamp(1.2rem, 3vw, 1.9rem);
border: 1px solid var(--grn-07-line);
border-radius: 16px;
background: var(--grn-07-surf);
}
.grn-07__list {
margin: 0;
padding-inline-start: 1.3rem;
display: grid;
gap: 0.55rem;
color: var(--grn-07-mut);
font-size: 0.92rem;
}
.grn-07__list b {
color: var(--grn-07-ink);
font-weight: 600;
}
.grn-07__list::marker {
color: var(--grn-07-c4);
}
@media (prefers-reduced-motion: reduce) {
.grn-07 * {
animation-duration: 1ms !important;
transition-duration: 1ms !important;
}
}
```Here's a working CSS Grain and Noise Texture from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Anti-Banding Grainy CSS Gradient
Source: https://codefronts.com/design-styles/css-grain-texture/anti-banding-grainy-css-gradient/
Banding is not a bug in your gradient, it is the 8-bit display pipeline showing through. The fix is the same one film and print have used for a century: add noise. This is a full-bleed dusk hero built the correct way — oklab interpolation, a grain dither layer, and an honest side-by-side against the untreated version.
## HTML
```html
<section class="grn-07" aria-labelledby="grn-07-title">
<div class="grn-07__theme">
<input class="grn-07__theme-input" type="checkbox" id="grn-07-theme">
<label class="grn-07__theme-label" for="grn-07-theme">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="grn-07__sr">Toggle dark theme</span>
</label>
</div>
<header class="grn-07__hero">
<div class="grn-07__hero-copy">
<p class="grn-07__kicker">Dusk · 21:04 · clear</p>
<h1 class="grn-07__h1" id="grn-07-title">A gradient with nowhere to hide</h1>
<p class="grn-07__lede">Deep blue to violet across a full viewport is the hardest banding case on the web. This one is interpolated in <code>oklab</code>, stopped three times through the dark third, and dithered with grain at 3.5%.</p>
<dl class="grn-07__specs">
<div><dt>Interpolation</dt><dd>in oklab</dd></div>
<div><dt>Dither</dt><dd>0.035</dd></div>
<div><dt>Stops</dt><dd>6</dd></div>
</dl>
</div>
</header>
<main class="grn-07__main">
<section class="grn-07__ab" aria-labelledby="grn-07-h-ab">
<div class="grn-07__ab-head">
<h2 class="grn-07__h2" id="grn-07-h-ab">The same ramp, treated and untreated</h2>
<p class="grn-07__note">If you cannot see the difference, turn your screen brightness down — banding lives in the shadows.</p>
</div>
<div class="grn-07__ab-grid">
<figure class="grn-07__panel grn-07__panel--raw">
<div class="grn-07__field"></div>
<figcaption><b>Untreated</b><span>sRGB interpolation, two stops, no dither</span></figcaption>
</figure>
<figure class="grn-07__panel grn-07__panel--fixed">
<div class="grn-07__field"></div>
<figcaption><b>Treated</b><span>oklab, six stops, 3.5% grain dither</span></figcaption>
</figure>
</div>
</section>
<section class="grn-07__steps" aria-labelledby="grn-07-h-steps">
<h2 class="grn-07__h2" id="grn-07-h-steps">Ship it in this order</h2>
<ol class="grn-07__list">
<li><b>Interpolate in oklab.</b> Perceptually even steps mean smaller bands before you dither anything.</li>
<li><b>Add stops where it bands.</b> Almost always the darkest third. Two extra stops halve the step size.</li>
<li><b>Dither with grain at 0.03.</b> Raise in 0.005 increments until the bands go. Stop there.</li>
<li><b>Check on an OLED phone at low brightness.</b> The harshest banding test that exists, and the one nobody runs.</li>
</ol>
</section>
</main>
</section>
```
## CSS
```css
.grn-07 {
--grn-07-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='grn07n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23grn07n)'/%3E%3C/svg%3E");
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-dither: 0.035;
--grn-07-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
--grn-07-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--grn-07-bg);
color: var(--grn-07-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.grn-07 *,
.grn-07 *::before,
.grn-07 *::after {
box-sizing: border-box;
}
.grn-07 :focus-visible {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07 code {
font-family: var(--grn-07-font-mono);
font-size: 0.9em;
}
.grn-07__sr {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
@media (prefers-color-scheme: light) {
.grn-07 {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #08060f;
--grn-07-surf: #120f1d;
--grn-07-line: #262138;
--grn-07-ink: #f2eefb;
--grn-07-mut: #a49bbd;
--grn-07-c1: #060b1f;
--grn-07-c1: oklch(18% 0.06 268);
--grn-07-c2: #21215c;
--grn-07-c2: oklch(32% 0.11 280);
--grn-07-c3: #6d3f8f;
--grn-07-c3: oklch(48% 0.15 310);
--grn-07-c4: #d97757;
--grn-07-c4: oklch(70% 0.13 40);
--grn-07-dither: 0.035;
}
}
@media (prefers-color-scheme: dark) {
.grn-07:has(.grn-07__theme-input:checked) {
--grn-07-bg: #f4f1fa;
--grn-07-surf: #ffffff;
--grn-07-line: #e2dcf0;
--grn-07-ink: #120f1d;
--grn-07-mut: #625a7c;
--grn-07-c1: #dfd8f2;
--grn-07-c1: oklch(89% 0.04 290);
--grn-07-c2: #a99ede;
--grn-07-c2: oklch(74% 0.09 288);
--grn-07-c3: #c68fb8;
--grn-07-c3: oklch(72% 0.1 340);
--grn-07-c4: #f0b083;
--grn-07-c4: oklch(82% 0.1 55);
--grn-07-dither: 0.03;
}
}
.grn-07__theme {
position: absolute;
inset-block-start: clamp(1rem, 3vw, 1.75rem);
inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
z-index: 40;
}
.grn-07__theme-input {
position: absolute;
inset: 0;
opacity: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
cursor: pointer;
}
.grn-07__theme-label {
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 999px;
background: rgba(10, 8, 20, 0.4);
color: #f2eefb;
cursor: pointer;
backdrop-filter: blur(6px);
}
.grn-07__theme-label svg {
inline-size: 20px;
block-size: 20px;
}
.grn-07__theme-input:focus-visible + .grn-07__theme-label {
outline: 2px solid var(--grn-07-c4);
outline-offset: 3px;
}
.grn-07__hero {
position: relative;
isolation: isolate;
overflow: hidden;
min-block-size: min(86svh, 46rem);
display: grid;
align-items: end;
background-image: radial-gradient(120% 90% at 78% 6%, color-mix(in oklab, var(--grn-07-c4) 55%, transparent) 0%, transparent 58%), linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 14%, var(--grn-07-c2) 38%, var(--grn-07-c2) 48%, var(--grn-07-c3) 74%, var(--grn-07-c4) 100%);
}
.grn-07__hero::after {
content: "";
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__hero-copy {
position: relative;
z-index: 2;
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(4rem, 12vw, 7rem);
display: grid;
gap: 0.85rem;
max-inline-size: 44rem;
}
.grn-07__kicker {
margin: 0;
font: 500 0.74rem/1 var(--grn-07-font-mono);
letter-spacing: 0.18em;
text-transform: uppercase;
color: color-mix(in oklab, var(--grn-07-ink) 78%, transparent);
}
.grn-07__h1 {
margin: 0;
font: 600 clamp(2.3rem, 7vw, 4.6rem)/0.99 var(--grn-07-font-display);
letter-spacing: -0.04em;
text-wrap: balance;
}
.grn-07__lede {
margin: 0;
max-inline-size: 34rem;
color: color-mix(in oklab, var(--grn-07-ink) 82%, transparent);
font-size: clamp(0.97rem, 2vw, 1.14rem);
text-wrap: pretty;
}
.grn-07__specs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0 0;
}
.grn-07__specs > div {
display: grid;
gap: 0.1rem;
padding: 0.55rem 0.85rem;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 10px;
background: rgba(8, 6, 15, 0.3);
backdrop-filter: blur(4px);
}
.grn-07__specs dt {
margin: 0;
font: 400 0.68rem/1 var(--grn-07-font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0.7;
}
.grn-07__specs dd {
margin: 0;
font: 500 0.88rem/1.2 var(--grn-07-font-mono);
}
.grn-07__main {
inline-size: min(70rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
margin-inline: auto;
padding-block: clamp(2.5rem, 7vw, 4.5rem);
display: grid;
gap: clamp(2rem, 5vw, 3rem);
}
.grn-07__ab {
display: grid;
gap: 1rem;
}
.grn-07__ab-head {
display: grid;
gap: 0.35rem;
padding-block-end: 0.7rem;
border-block-end: 1px solid var(--grn-07-line);
}
.grn-07__h2 {
margin: 0;
font: 600 clamp(1.15rem, 2.8vw, 1.5rem)/1.2 var(--grn-07-font-display);
letter-spacing: -0.02em;
}
.grn-07__note {
margin: 0;
color: var(--grn-07-mut);
font-size: 0.88rem;
text-wrap: pretty;
}
.grn-07__ab-grid {
display: grid;
gap: clamp(0.8rem, 2vw, 1.3rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
}
.grn-07__panel {
margin: 0;
display: grid;
gap: 0.6rem;
min-inline-size: 0;
}
.grn-07__field {
position: relative;
isolation: isolate;
overflow: hidden;
block-size: clamp(11rem, 26vw, 17rem);
border-radius: 14px;
}
.grn-07__panel--raw .grn-07__field {
background-image: linear-gradient(168deg, var(--grn-07-c1), var(--grn-07-c3));
}
.grn-07__panel--fixed .grn-07__field {
background-image: linear-gradient(in oklab 168deg, var(--grn-07-c1) 0%, var(--grn-07-c1) 12%, var(--grn-07-c2) 34%, var(--grn-07-c2) 46%, var(--grn-07-c3) 78%, var(--grn-07-c3) 100%);
}
.grn-07__panel--fixed .grn-07__field::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background-image: var(--grn-07-noise);
opacity: var(--grn-07-dither);
mix-blend-mode: overlay;
}
.grn-07__panel figcaption {
display: grid;
gap: 0.15rem;
}
.grn-07__panel figcaption b {
font: 600 0.95rem/1.2 var(--grn-07-font-display);
}
.grn-07__panel figcaption span {
color: var(--grn-07-mut);
font: 400 0.8rem/1.4 var(--grn-07-font-mono);
}
.grn-07__steps {
display: grid;
gap: 0.9rem;
padding: clamp(1.2rem, 3vw, 1.9rem);
border: 1px solid var(--grn-07-line);
border-radius: 16px;
background: var(--grn-07-surf);
}
.grn-07__list {
margin: 0;
padding-inline-start: 1.3rem;
display: grid;
gap: 0.55rem;
color: var(--grn-07-mut);
font-size: 0.92rem;
}
.grn-07__list b {
color: var(--grn-07-ink);
font-weight: 600;
}
.grn-07__list::marker {
color: var(--grn-07-c4);
}
@media (prefers-reduced-motion: reduce) {
.grn-07 * {
animation-duration: 1ms !important;
transition-duration: 1ms !important;
}
}
```How this works
Why gradients band at all. An 8-bit channel has 256 steps. A gradient spanning 400 pixels between two near colours may only cross 12 of them, so the browser paints 12 flat strips of 33 pixels each. Your eye is extremely good at detecting edges, so it finds those strips instantly — worst of all in dark blues and dark greys, which is why hero sections and dark modes band the most visibly. No amount of extra colour stops fully removes it, because the output precision is the constraint, not the input.
Noise is a dither, and dithering is the actual fix. Add a small random perturbation and each pixel rounds to a slightly different quantisation step, so the hard boundary between two bands becomes a stochastic mix of the two. The eye integrates that mix and reads a smooth ramp. The opacity needed is tiny — 0.02 to 0.05 — because you are not adding texture, you are adding uncertainty. This is exactly what image encoders, printers and film have always done.
Do the colour-space work first. linear-gradient(in oklab, …) interpolates in a perceptually uniform space, which spreads the available steps where your eye needs them instead of clustering them. It does not remove banding, but it makes the remaining bands smaller and more evenly sized — which means a fainter dither can hide them. Adding two or three intermediate stops in the darkest third helps for the same reason.
Ship it in this order. One: interpolate in oklab. Two: add intermediate stops where banding shows. Three: add a grain dither at 0.03 and raise it only until the bands disappear. Four: check on a real OLED phone at low brightness, which is the harshest banding test that exists, and on which almost every untreated dark gradient on the web fails.
Make it yours
- Change
--grn-07-ditherfrom0.035upward in0.005steps until banding disappears — resist going past0.06. - Swap
in oklabforin oklch longer hueto route the ramp the long way around the hue wheel for a much richer dusk. - Add or move intermediate stops in the darkest third; that is where the eye finds bands first.
- Increase the grain
baseFrequencyto1.1if the dither itself becomes visible as texture rather than disappearing. - Set the A/B panel's
--grn-07-ditherto0permanently if you would rather ship a single hero than a comparison. - Recolour with the three
--grn-07-c*pairs; the dither is neutral and works against any ramp.
Gotchas — read before shipping
- Banding is display-dependent. A gradient that looks clean on your 10-bit monitor can band badly on an 8-bit laptop panel — test on the cheap screen, not the good one.
- Dither opacity above about
0.08stops being invisible and starts reading as deliberate grain. That is a valid look, but it is no longer a fix. - A dither layer applied with
normalblend mode greys the gradient out. Useoverlayorsoft-light. - Very large gradients with a grain layer promote to their own compositing layer at full viewport size. On a 3x phone that is a multi-megabyte GPU texture — bound the region if you can.
in oklabsilently falls back to sRGB interpolation in older engines, which is fine, but it means your banding fix is weaker there. Keep the dither doing the heavy lifting.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by :has(), oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
Gradient interpolation with in oklab shipped in Chrome 111, Safari 16.2 and Firefox 113; below that the gradient renders in sRGB and still works, just with slightly larger bands. The grain dither itself has no floor. Safari on macOS renders gradients with subtly better precision than Chrome, so tune the dither on Chrome and it will be safe on both.