Mesh Gradient CSS, Grain Left In on Purpose
Mesh gradient CSS blends blurred color blobs across a dark canvas like an aurora, then adds film grain so it doesn't read as another AI-smooth template —
The one-page build below stitches all nine sections together. Scroll through it first and watch where the aurora gets dense and where it thins out.

Each mesh gradient css animation also appears on its own in the grid. The order isn't by popularity — it follows how a visitor actually reads a page: notice it, skim the features, check the price, trust the review, then click. Sculpt the same softness into volumes and the claymorphism landing kit appears; sharpen the edges instead and the neobrutalism landing sections stand on the opposite side.
01 hero-aurora
02 nav-aurora
03 card-grid-aurora
04 feature-list-aurora
05 pricing-aurora
06 testimonial-aurora
07 cta-band-aurora
08 footer-aurora
09 buttons-inputs-aurora
Auto-plays · click a tile to jump to its section · all nine in one zip
- 01 Drifting orb background hero
- 02 Transparent nav
- 03 Gradient-border cards
- 04 Gradient icon list
- 05 Gradient-highlighted plan
- 06 Blurred-background quote
- 07 Full-bleed aurora band
- 08 Grainy dark footer
- 09 Gradient buttons, glow focus
01Hero
Three blobs drifting behind
Three blurred, colored blobs drift behind the headline at slightly different rhythms, overlapping as they move.

<main class="stage">
<section class="hero-a">
<div class="hero-a__orb hero-a__orb--1" aria-hidden="true"></div>
<div class="hero-a__orb hero-a__orb--2" aria-hidden="true"></div>
<div class="hero-a__orb hero-a__orb--3" aria-hidden="true"></div>
<p class="hero-a__eyebrow">AURORA</p>
<h2 class="hero-a__title">Manage subscribers from one screen</h2>
<button class="hero-a__cta" type="button">Start for free</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 오브 세 개가 한 바퀴 도는 시간
$easing: steps(4, jump-none); // 부드러운 곡선은 블러 뒤에서 프레임 간 변화가 너무 작아 안 잡힌다
$color: $subject-blue; // 오브 색 하나
@include stage($stage-ink);
.hero-a {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: clamp(260px, 84vw, 400px);
min-height: clamp(172px, 48vw, 220px);
border-radius: $r-card;
background: $stage-ink;
box-shadow: $shadow-raised;
padding: $sp-6;
display: flex;
flex-direction: column;
justify-content: center;
gap: $sp-3;
}
// filter: blur 로 흐린 오브를 겹치고, transform(translate·rotate)으로만 움직인다
.hero-a__orb {
position: absolute;
width: 46%;
aspect-ratio: 1;
border-radius: 50%;
filter: blur(28px);
opacity: .85;
}
.hero-a__orb--1 { background: radial-gradient($color 0%, $color 35%, transparent 68%); top: -10%; left: 4%; animation: driftA $duration $easing infinite; }
.hero-a__orb--2 { background: radial-gradient($stage-yellow 0%, $stage-yellow 35%, transparent 68%); bottom: -14%; right: 0%; animation: driftB $duration $easing infinite; animation-delay: -.6s; }
.hero-a__orb--3 { background: radial-gradient($stage-orange 0%, $stage-orange 35%, transparent 68%); top: 26%; right: 26%; animation: driftC $duration $easing infinite; animation-delay: -1.2s; }
// 그레인 — 정지 텍스처. 프레임마다 안 바뀐다 (움직이는 건 오브 하나)
.hero-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .35;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($stage-paper, .6) .5px, transparent .5px),
radial-gradient(rgba($stage-ink, .6) .5px, transparent .5px);
background-size: 3px 3px, 5px 5px;
background-position: 0 0, 1px 2px;
border-radius: inherit;
}
.hero-a__eyebrow { position: relative; z-index: 1; margin: 0; color: $stage-yellow; font: 700 clamp(10px, 2.4vw, 12px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; letter-spacing: .06em; text-transform: uppercase; }
.hero-a__title { position: relative; z-index: 1; margin: 0; color: $stage-paper; font: 700 clamp(16px, 4.2vw, 20px)/1.25 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.hero-a__cta { position: relative; z-index: 1; align-self: flex-start; border: 0; border-radius: $r-pill; background: $subject-cream; color: $stage-ink; padding: $sp-2 $sp-5; font-weight: 700; font-size: clamp(11px, 2.6vw, 13px); line-height: 1; }
@keyframes driftA { 0% { transform: translate(0,0) rotate(0deg); } 50% { transform: translate(22px, 16px) rotate(20deg); } 100% { transform: translate(0,0) rotate(0deg); } }
@keyframes driftB { 0% { transform: translate(0,0) rotate(0deg); } 50% { transform: translate(-24px, -14px) rotate(-18deg); } 100% { transform: translate(0,0) rotate(0deg); } }
@keyframes driftC { 0% { transform: translate(0,0) rotate(0deg); } 50% { transform: translate(18px, -20px) rotate(16deg); } 100% { transform: translate(0,0) rotate(0deg); } }
@media (prefers-reduced-motion: reduce) {
.hero-a__orb { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.hero-a {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: clamp(260px, 84vw, 400px);
min-height: clamp(172px, 48vw, 220px);
border-radius: 16px;
background: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 24px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 12px;
}
.hero-a__orb {
position: absolute;
width: 46%;
aspect-ratio: 1;
border-radius: 50%;
filter: blur(28px);
opacity: 0.85;
}
.hero-a__orb--1 {
background: radial-gradient(#2f6df6 0%, #2f6df6 35%, transparent 68%);
top: -10%;
left: 4%;
animation: driftA 2s steps(4, jump-none) infinite;
}
.hero-a__orb--2 {
background: radial-gradient(#ffd23f 0%, #ffd23f 35%, transparent 68%);
bottom: -14%;
right: 0%;
animation: driftB 2s steps(4, jump-none) infinite;
animation-delay: -0.6s;
}
.hero-a__orb--3 {
background: radial-gradient(#ff4d1f 0%, #ff4d1f 35%, transparent 68%);
top: 26%;
right: 26%;
animation: driftC 2s steps(4, jump-none) infinite;
animation-delay: -1.2s;
}
.hero-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.35;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 255, 255, 0.6) 0.5px, transparent 0.5px), radial-gradient(rgba(23, 20, 26, 0.6) 0.5px, transparent 0.5px);
background-size: 3px 3px, 5px 5px;
background-position: 0 0, 1px 2px;
border-radius: inherit;
}
.hero-a__eyebrow {
position: relative;
z-index: 1;
margin: 0;
color: #ffd23f;
font: 700 clamp(10px, 2.4vw, 12px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.hero-a__title {
position: relative;
z-index: 1;
margin: 0;
color: #ffffff;
font: 700 clamp(16px, 4.2vw, 20px)/1.25 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.hero-a__cta {
position: relative;
z-index: 1;
align-self: flex-start;
border: 0;
border-radius: 999px;
background: #fff7e6;
color: #17141a;
padding: 8px 20px;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
@keyframes driftA {
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(22px, 16px) rotate(20deg);
}
100% {
transform: translate(0, 0) rotate(0deg);
}
}
@keyframes driftB {
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(-24px, -14px) rotate(-18deg);
}
100% {
transform: translate(0, 0) rotate(0deg);
}
}
@keyframes driftC {
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(18px, -20px) rotate(16deg);
}
100% {
transform: translate(0, 0) rotate(0deg);
}
}
@media (prefers-reduced-motion: reduce) {
.hero-a__orb {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraHero.module.scss";
export type AuroraHeroProps = {
eyebrow?: string;
title?: string;
cta?: string;
/** 한 주기(기본 2000ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraHero({
eyebrow = "AURORA",
title = "Manage subscribers from one screen",
cta = "Start for free",
duration = 2000,
easing = "steps(4, jump-none)",
color = "#2f6df6",
}: AuroraHeroProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.hero} style={vars}>
<div className={`${styles.orb} ${styles.orb1}`} aria-hidden="true" />
<div className={`${styles.orb} ${styles.orb2}`} aria-hidden="true" />
<div className={`${styles.orb} ${styles.orb3}`} aria-hidden="true" />
<p className={styles.eyebrow}>{eyebrow}</p>
<h2 className={styles.title}>{title}</h2>
<button type="button" className={styles.cta}>{cta}</button>
</section>
);
}01 · Drifting orb background hero — Measured area 38.218% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password
The card never moves
The card itself stays a fixed dark ink; the aurora is only ever light passing over it.
.hero-a__orb { position: absolute; width: 46%; aspect-ratio: 1;
border-radius: 50%; filter: blur(28px); opacity: .85; }
.hero-a__orb--1 {
background: radial-gradient($color 0%, $color 35%, transparent 68%);
top: -10%; left: 4%;
animation: driftA $duration steps(4, jump-none) infinite;
}
@keyframes driftA {
0% { transform: translate(0,0) rotate(0deg); }
50% { transform: translate(22px, 16px) rotate(20deg); }
100% { transform: translate(0,0) rotate(0deg); }
}
02Navigation
A nearly transparent nav
The nav background is nearly transparent — just a thin sheet of ink-tinted glass underneath.

<main class="stage">
<nav class="nav-a">
<span class="nav-a__brand">Aurora</span>
<div class="nav-a__menu">
<span class="nav-a__pill" aria-hidden="true"></span>
<span class="nav-a__label">Home</span>
<span class="nav-a__label">Features</span>
<span class="nav-a__label">Pricing</span>
</div>
<button class="nav-a__cta" type="button">Try it free</button>
</nav>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-yellow);
.nav-a {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: $sp-3;
width: clamp(240px, 82vw, 380px);
padding: $sp-2 $sp-3;
border-radius: $r-pill;
background: rgba($stage-ink, .62); // .38 은 흰 글자가 2.98:1 이었다 — AA 4.5 (brand 6.49 · label 5.35)
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba($stage-paper, .18);
overflow: hidden;
}
.nav-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .3;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($stage-paper, .6) .5px, transparent .5px),
radial-gradient(rgba($stage-ink, .6) .5px, transparent .5px);
background-size: 3px 3px, 5px 5px;
border-radius: inherit;
}
.nav-a__brand { position: relative; z-index: 1; padding-left: $sp-2; color: $stage-paper; font: 700 clamp(12px, 3vw, 14px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.nav-a__menu {
position: relative;
z-index: 1;
flex: 1;
display: flex;
background: rgba($stage-paper, .08);
border-radius: $r-pill;
padding: $sp-1;
}
// 인디케이터는 퍼센트 폭(항목 3등분)으로 움직인다 — 고정 px 짐작값 금지 (BD19)
.nav-a__pill {
position: absolute;
top: $sp-1;
bottom: $sp-1;
left: $sp-1;
width: calc((100% - #{$sp-1 * 2}) / 3);
border-radius: $r-pill;
background: $color;
animation: pillSlide $duration $easing infinite;
}
.nav-a__label { position: relative; z-index: 1; flex: 1; text-align: center; padding: $sp-2 0; color: $stage-paper; font-weight: 600; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1; }
.nav-a__cta { position: relative; z-index: 1; border: 0; border-radius: $r-pill; background: $stage-paper; color: $stage-ink; padding: $sp-2 $sp-4; font-weight: 700; font-size: clamp(10.5px, 2.6vw, 12px); line-height: 1; }
@keyframes pillSlide {
0%, 12% { transform: translateX(0%); }
38%, 50% { transform: translateX(100%); }
76%, 88% { transform: translateX(200%); }
100% { transform: translateX(0%); }
}
@media (prefers-reduced-motion: reduce) {
.nav-a__pill { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.nav-a {
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 12px;
width: clamp(240px, 82vw, 380px);
padding: 8px 12px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.62);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
overflow: hidden;
}
.nav-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.3;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 255, 255, 0.6) 0.5px, transparent 0.5px), radial-gradient(rgba(23, 20, 26, 0.6) 0.5px, transparent 0.5px);
background-size: 3px 3px, 5px 5px;
border-radius: inherit;
}
.nav-a__brand {
position: relative;
z-index: 1;
padding-left: 8px;
color: #ffffff;
font: 700 clamp(12px, 3vw, 14px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.nav-a__menu {
position: relative;
z-index: 1;
flex: 1;
display: flex;
background: rgba(255, 255, 255, 0.08);
border-radius: 999px;
padding: 4px;
}
.nav-a__pill {
position: absolute;
top: 4px;
bottom: 4px;
left: 4px;
width: calc((100% - 8px) / 3);
border-radius: 999px;
background: #2f6df6;
animation: pillSlide 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.nav-a__label {
position: relative;
z-index: 1;
flex: 1;
text-align: center;
padding: 8px 0;
color: #ffffff;
font-weight: 600;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
}
.nav-a__cta {
position: relative;
z-index: 1;
border: 0;
border-radius: 999px;
background: #ffffff;
color: #17141a;
padding: 8px 16px;
font-weight: 700;
font-size: clamp(10.5px, 2.6vw, 12px);
line-height: 1;
}
@keyframes pillSlide {
0%, 12% {
transform: translateX(0%);
}
38%, 50% {
transform: translateX(100%);
}
76%, 88% {
transform: translateX(200%);
}
100% {
transform: translateX(0%);
}
}
@media (prefers-reduced-motion: reduce) {
.nav-a__pill {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraNav.module.scss";
export type AuroraNavProps = {
brand?: string;
items?: string[];
cta?: string;
activeIndex?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraNav({
brand = "Aurora",
items = ["Home", "Features", "Pricing"],
cta = "Try it free",
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: AuroraNavProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<nav className={styles.nav} style={vars}>
<span className={styles.brand}>{brand}</span>
<div className={styles.menu}>
<span className={styles.pill} aria-hidden="true" />
{items.map((label) => (
<span key={label} className={styles.label}>{label}</span>
))}
</div>
<button type="button" className={styles.cta}>{cta}</button>
</nav>
);
}02 · Transparent nav — Measured area 3.459% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password
Thirds, not guessed pixels
The pill marking the active item is sized by splitting the three links into equal thirds by percentage, not by a guessed pixel width.
.nav-a {
background: rgba($stage-ink, .38);
backdrop-filter: blur(10px);
border: 1px solid rgba($stage-paper, .18);
}
.nav-a__pill {
position: absolute; top: $sp-1; bottom: $sp-1; left: $sp-1;
width: calc((100% - #{$sp-1 * 2}) / 3);
border-radius: $r-pill; background: $color;
animation: pillSlide $duration $ease-spring infinite;
}
03Card grid
A stripe only at the border
Only a ring at the card's border is cut out to show a three-color aurora stripe, and that stripe keeps flowing.

<main class="stage">
<article class="card-a">
<div class="card-a__body">
<span class="card-a__icon" aria-hidden="true"></span>
<h3 class="card-a__title">Real-time sync</h3>
<p class="card-a__desc">Stays in sync across every device.</p>
</div>
</article>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: linear;
$color: $subject-blue;
@include stage($stage-orange);
.stage { padding: clamp(8px, 3vw, 20px); }
// 테두리 자리에만 보이게 잘라낸 링 위에 오로라 3색 줄무늬를 흘려보낸다
.card-a {
position: relative;
box-sizing: border-box;
width: min(240px, 78vw);
border-radius: $r-card;
padding: $sp-1;
isolation: isolate;
}
.card-a::before {
content: "";
position: absolute;
inset: 0;
border-radius: $r-card;
padding: $sp-1;
background: repeating-linear-gradient(120deg, $color 0 26px, $stage-yellow 26px 52px, $stage-orange 52px 78px, transparent 78px 104px);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: sweep $duration $easing infinite;
}
.card-a__body {
position: relative;
overflow: hidden;
border-radius: calc($r-card - $sp-1);
background: $stage-ink;
padding: $sp-5;
display: flex;
flex-direction: column;
gap: $sp-2;
}
.card-a__body::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .3;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($stage-paper, .6) .5px, transparent .5px),
radial-gradient(rgba($stage-ink, .6) .5px, transparent .5px);
background-size: 3px 3px, 5px 5px;
}
.card-a__icon { position: relative; z-index: 1; width: $sp-8; height: $sp-8; border-radius: $r-sm; background: radial-gradient(circle at 30% 30%, $color, $stage-orange); }
.card-a__title { position: relative; z-index: 1; margin: 0; color: $stage-paper; font: 700 clamp(13px, 3.2vw, 15px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.card-a__desc { position: relative; z-index: 1; margin: 0; color: rgba($stage-paper, .7); font-weight: 500; font-size: clamp(11px, 2.6vw, 12.5px); line-height: 1.4; }
@keyframes sweep { to { background-position: 208px 0; } }
@media (prefers-reduced-motion: reduce) {
.card-a::before { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
padding: clamp(8px, 3vw, 20px);
}
.card-a {
position: relative;
box-sizing: border-box;
width: min(240px, 78vw);
border-radius: 16px;
padding: 4px;
isolation: isolate;
}
.card-a::before {
content: "";
position: absolute;
inset: 0;
border-radius: 16px;
padding: 4px;
background: repeating-linear-gradient(120deg, #2f6df6 0 26px, #ffd23f 26px 52px, #ff4d1f 52px 78px, transparent 78px 104px);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: sweep 2s linear infinite;
}
.card-a__body {
position: relative;
overflow: hidden;
border-radius: 12px;
background: #17141a;
padding: 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.card-a__body::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.3;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 255, 255, 0.6) 0.5px, transparent 0.5px), radial-gradient(rgba(23, 20, 26, 0.6) 0.5px, transparent 0.5px);
background-size: 3px 3px, 5px 5px;
}
.card-a__icon {
position: relative;
z-index: 1;
width: 32px;
height: 32px;
border-radius: 8px;
background: radial-gradient(circle at 30% 30%, #2f6df6, #ff4d1f);
}
.card-a__title {
position: relative;
z-index: 1;
margin: 0;
color: #ffffff;
font: 700 clamp(13px, 3.2vw, 15px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.card-a__desc {
position: relative;
z-index: 1;
margin: 0;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
font-size: clamp(11px, 2.6vw, 12.5px);
line-height: 1.4;
}
@keyframes sweep {
to {
background-position: 208px 0;
}
}
@media (prefers-reduced-motion: reduce) {
.card-a::before {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraCard.module.scss";
export type AuroraCardProps = {
title?: string;
desc?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraCard({
title = "Real-time sync",
desc = "Stays in sync across every device.",
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: AuroraCardProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<article className={styles.card} style={vars}>
<div className={styles.body}>
<span className={styles.icon} aria-hidden="true" />
<h3 className={styles.title}>{title}</h3>
<p className={styles.desc}>{desc}</p>
</div>
</article>
);
}03 · Gradient-border cards — Measured area 2.081% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Why the eye stays on the edge
The card body underneath stays a fixed dark ink so the eye stays on the edge.
.card-a::before {
content: ""; position: absolute; inset: 0;
border-radius: $r-card; padding: $sp-1;
background: repeating-linear-gradient(120deg,
$color 0 26px, $stage-yellow 26px 52px,
$stage-orange 52px 78px, transparent 78px 104px);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
animation: sweep $duration linear infinite;
}
@keyframes sweep { to { background-position: 208px 0; } }
04Feature list
One badge per feature
Each feature gets one round badge, and only the gradient inside that badge drifts side to side.

<main class="stage">
<ul class="feat-a">
<li class="feat-a__row">
<span class="feat-a__icon" aria-hidden="true"></span>
<p class="feat-a__text">Fast deploys</p>
</li>
<li class="feat-a__row">
<span class="feat-a__icon" aria-hidden="true"></span>
<p class="feat-a__text">Auto backups</p>
</li>
<li class="feat-a__row">
<span class="feat-a__icon" aria-hidden="true"></span>
<p class="feat-a__text">Real-time collaboration</p>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-paper);
.feat-a {
// 태그가 <ul> 이라 UA 기본 margin·padding(list-style)부터 지운다 — 안 지우면 320px 세로 넘침의 숨은 원인이 된다
margin: 0;
list-style: none;
position: relative;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $sp-3;
width: clamp(240px, 80vw, 360px);
padding: $sp-5;
border-radius: $r-card;
background: $stage-ink;
box-shadow: $shadow-raised;
}
.feat-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .3;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($stage-paper, .6) .5px, transparent .5px),
radial-gradient(rgba($stage-ink, .6) .5px, transparent .5px);
background-size: 3px 3px, 5px 5px;
border-radius: inherit;
}
.feat-a__row { position: relative; z-index: 1; margin: 0; display: flex; align-items: center; gap: $sp-3; }
// 아이콘 배지 안 그라디언트가 좌우로 흐른다 (background-position 만 움직인다)
.feat-a__icon {
flex: none;
width: $sp-8;
height: $sp-8;
border-radius: $r-pill;
background: linear-gradient(120deg, $color, $stage-yellow, $stage-orange, $color);
background-size: 200% 100%;
animation: iconFlow $duration $easing infinite;
}
.feat-a__text { margin: 0; color: $stage-paper; font: 600 clamp(11.5px, 2.8vw, 13px)/1.4 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
@keyframes iconFlow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
@media (prefers-reduced-motion: reduce) {
.feat-a__icon { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.feat-a {
margin: 0;
list-style: none;
position: relative;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 12px;
width: clamp(240px, 80vw, 360px);
padding: 20px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.feat-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.3;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 255, 255, 0.6) 0.5px, transparent 0.5px), radial-gradient(rgba(23, 20, 26, 0.6) 0.5px, transparent 0.5px);
background-size: 3px 3px, 5px 5px;
border-radius: inherit;
}
.feat-a__row {
position: relative;
z-index: 1;
margin: 0;
display: flex;
align-items: center;
gap: 12px;
}
.feat-a__icon {
flex: none;
width: 32px;
height: 32px;
border-radius: 999px;
background: linear-gradient(120deg, #2f6df6, #ffd23f, #ff4d1f, #2f6df6);
background-size: 200% 100%;
animation: iconFlow 2s ease-out infinite;
}
.feat-a__text {
margin: 0;
color: #ffffff;
font: 600 clamp(11.5px, 2.8vw, 13px)/1.4 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
@keyframes iconFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.feat-a__icon {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraFeatureList.module.scss";
export type AuroraFeatureListProps = {
items?: string[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraFeatureList({
items = ["Fast deploys", "Auto backups", "Real-time collaboration"],
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: AuroraFeatureListProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<ul className={styles.list} style={vars}>
{items.map((text) => (
<li key={text} className={styles.row}>
<span className={styles.icon} aria-hidden="true" />
<p className={styles.text}>{text}</p>
</li>
))}
</ul>
);
}04 · Gradient icon list — Measured area 1.75% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password
Why the whole row stays still
Moving the whole row would be distracting; one small drifting dot per line is enough to hold the eye.
.feat-a__icon {
width: $sp-8; height: $sp-8; border-radius: $r-pill;
background: linear-gradient(120deg, $color, $stage-yellow, $stage-orange, $color);
background-size: 200% 100%;
animation: iconFlow $duration ease-out infinite;
}
@keyframes iconFlow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
05Pricing
Only the recommended plan breathes
Only the recommended plan gets an aurora gradient that brightens and dims like breathing, drifting slightly as it goes.

<main class="stage">
<section class="pricing-a">
<div class="pricing-a__plan">
<p class="pricing-a__name">Free</p>
<p class="pricing-a__price">₩0</p>
</div>
<div class="pricing-a__plan pricing-a__plan--pro">
<span class="pricing-a__badge">Recommended</span>
<p class="pricing-a__name">Pro</p>
<p class="pricing-a__price">$19/mo</p>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: steps(4, jump-none); // 부드러운 곡선은 프레임 간 변화가 너무 작아 안 잡혀 큰 걸음으로 끊어 보낸다
$color: $subject-blue;
@include stage($stage-ink);
.pricing-a { display: flex; align-items: flex-end; gap: $sp-4; width: clamp(240px, 82vw, 380px); }
.pricing-a__plan {
flex: 1;
border-radius: $r-card;
padding: $sp-5 $sp-4;
background: rgba($stage-paper, .06);
border: 1px solid rgba($stage-paper, .12);
}
.pricing-a__plan--pro {
position: relative;
overflow: hidden;
flex: 1.15;
padding-top: $sp-8;
background: $stage-ink;
border-color: rgba($color, .5);
box-shadow: $shadow-raised;
}
// 추천 플랜만 오로라 그라디언트가 숨 쉬듯 밝기·위치를 오간다
.pricing-a__plan--pro::before {
content: "";
position: absolute;
inset: -20%;
background: radial-gradient(circle at 30% 20%, rgba($color, .9), transparent 60%),
radial-gradient(circle at 75% 80%, rgba($stage-orange, .85), transparent 60%);
animation: breathe $duration $easing infinite;
}
.pricing-a__badge { position: relative; z-index: 1; display: inline-block; margin-bottom: $sp-2; border-radius: $r-pill; background: $color; color: $stage-paper; padding: $sp-1 $sp-3; font: 700 clamp(9.5px, 2.2vw, 11px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.pricing-a__name { position: relative; z-index: 1; margin: 0 0 $sp-1; color: $stage-paper; font: 700 clamp(12px, 3vw, 14px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.pricing-a__price { position: relative; z-index: 1; margin: 0; color: rgba($stage-paper, .75); font-weight: 600; font-size: clamp(11px, 2.6vw, 12.5px); line-height: 1.3; }
@keyframes breathe { 0%, 100% { opacity: .3; transform: translate(0, 0); } 50% { opacity: 1; transform: translate(-8%, 8%); } }
@media (prefers-reduced-motion: reduce) {
.pricing-a__plan--pro::before { animation: none; opacity: .8; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pricing-a {
display: flex;
align-items: flex-end;
gap: 16px;
width: clamp(240px, 82vw, 380px);
}
.pricing-a__plan {
flex: 1;
border-radius: 16px;
padding: 20px 16px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.pricing-a__plan--pro {
position: relative;
overflow: hidden;
flex: 1.15;
padding-top: 32px;
background: #17141a;
border-color: rgba(47, 109, 246, 0.5);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.pricing-a__plan--pro::before {
content: "";
position: absolute;
inset: -20%;
background: radial-gradient(circle at 30% 20%, rgba(47, 109, 246, 0.9), transparent 60%), radial-gradient(circle at 75% 80%, rgba(255, 77, 31, 0.85), transparent 60%);
animation: breathe 2s steps(4, jump-none) infinite;
}
.pricing-a__badge {
position: relative;
z-index: 1;
display: inline-block;
margin-bottom: 8px;
border-radius: 999px;
background: #2f6df6;
color: #ffffff;
padding: 4px 12px;
font: 700 clamp(9.5px, 2.2vw, 11px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.pricing-a__name {
position: relative;
z-index: 1;
margin: 0 0 4px;
color: #ffffff;
font: 700 clamp(12px, 3vw, 14px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.pricing-a__price {
position: relative;
z-index: 1;
margin: 0;
color: rgba(255, 255, 255, 0.75);
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12.5px);
line-height: 1.3;
}
@keyframes breathe {
0%, 100% {
opacity: 0.3;
transform: translate(0, 0);
}
50% {
opacity: 1;
transform: translate(-8%, 8%);
}
}
@media (prefers-reduced-motion: reduce) {
.pricing-a__plan--pro::before {
animation: none;
opacity: 0.8;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraPricing.module.scss";
export type AuroraPricingProps = {
freeName?: string;
freePrice?: string;
badge?: string;
proName?: string;
proPrice?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraPricing({
freeName = "Free",
freePrice = "$0",
badge = "Recommended",
proName = "Pro",
proPrice = "$19/mo",
duration = 2000,
easing = "steps(4, jump-none)",
color = "#2f6df6",
}: AuroraPricingProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.pricing} style={vars}>
<div className={styles.plan}>
<p className={styles.name}>{freeName}</p>
<p className={styles.price}>{freePrice}</p>
</div>
<div className={`${styles.plan} ${styles.pro}`}>
<span className={styles.badge}>{badge}</span>
<p className={styles.name}>{proName}</p>
<p className={styles.price}>{proPrice}</p>
</div>
</section>
);
}05 · Gradient-highlighted plan — Measured area 15.09% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password
Contrast does the selling
The other plan stays a faint glass tile so the contrast does the selling.
.pricing-a__plan--pro::before {
content: ""; position: absolute; inset: -20%;
background:
radial-gradient(circle at 30% 20%, rgba($color, .9), transparent 60%),
radial-gradient(circle at 75% 80%, rgba($stage-orange, .85), transparent 60%);
animation: breathe $duration steps(4, jump-none) infinite;
}
@keyframes breathe {
0%, 100% { opacity: .3; transform: translate(0, 0); }
50% { opacity: 1; transform: translate(-8%, 8%); }
}
06Testimonial
Blobs behind frosted glass
Two blurred aurora blobs drift slowly behind a frosted quote card.

<main class="stage">
<figure class="test-a">
<div class="test-a__orb test-a__orb--1" aria-hidden="true"></div>
<div class="test-a__orb test-a__orb--2" aria-hidden="true"></div>
<blockquote class="test-a__card">
<p class="test-a__quote">"Our launch got twice as fast thanks to this."</p>
<figcaption class="test-a__name">Kim Do-hyun · Product Lead</figcaption>
</blockquote>
</figure>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: steps(4, jump-none); // 부드러운 곡선은 블러 뒤에서 프레임 간 변화가 너무 작아 안 잡힌다
$color: $stage-orange;
@include stage($stage-yellow);
// 태그가 <figure> 라 UA 기본 margin(1em 40px)부터 지운다 — 안 지우면 320px 세로 넘침의 숨은 원인이 된다
.test-a { margin: 0; position: relative; box-sizing: border-box; width: clamp(240px, 82vw, 360px); padding: $sp-5; }
// 흐린 오로라 덩어리 두 개가 유리 카드 뒤에서 천천히 떠돈다
.test-a__orb { position: absolute; width: 50%; aspect-ratio: 1; border-radius: 50%; filter: blur(20px); opacity: 1; }
.test-a__orb--1 { background: radial-gradient($subject-blue 0%, $subject-blue 45%, transparent 72%); top: -8%; left: -8%; animation: floatA $duration $easing infinite; }
.test-a__orb--2 { background: radial-gradient($color 0%, $color 45%, transparent 72%); bottom: -8%; right: -6%; animation: floatB $duration $easing infinite; animation-delay: -1s; }
.test-a__card {
// <blockquote> 의 UA 기본 margin(1em 40px)도 지운다
margin: 0;
position: relative;
overflow: hidden;
border-radius: $r-card;
padding: $sp-5;
background: rgba($stage-ink, .22);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
border: 1px solid rgba($stage-paper, .18);
}
.test-a__card::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .25;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($stage-paper, .6) .5px, transparent .5px),
radial-gradient(rgba($stage-ink, .6) .5px, transparent .5px);
background-size: 3px 3px, 5px 5px;
}
.test-a__quote { position: relative; z-index: 1; margin: 0 0 $sp-3; color: $stage-paper; font: 600 clamp(12px, 3vw, 13.5px)/1.5 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.test-a__name { position: relative; z-index: 1; margin: 0; color: $stage-paper; font-weight: 600; font-size: clamp(10.5px, 2.4vw, 11.5px); line-height: 1.3; }
@keyframes floatA { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(24px, 28px); } }
@keyframes floatB { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(-28px, -20px); } }
@media (prefers-reduced-motion: reduce) {
.test-a__orb { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.test-a {
margin: 0;
position: relative;
box-sizing: border-box;
width: clamp(240px, 82vw, 360px);
padding: 20px;
}
.test-a__orb {
position: absolute;
width: 50%;
aspect-ratio: 1;
border-radius: 50%;
filter: blur(20px);
opacity: 1;
}
.test-a__orb--1 {
background: radial-gradient(#2f6df6 0%, #2f6df6 45%, transparent 72%);
top: -8%;
left: -8%;
animation: floatA 2s steps(4, jump-none) infinite;
}
.test-a__orb--2 {
background: radial-gradient(#ff4d1f 0%, #ff4d1f 45%, transparent 72%);
bottom: -8%;
right: -6%;
animation: floatB 2s steps(4, jump-none) infinite;
animation-delay: -1s;
}
.test-a__card {
margin: 0;
position: relative;
overflow: hidden;
border-radius: 16px;
padding: 20px;
background: rgba(23, 20, 26, 0.22);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.test-a__card::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.25;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 255, 255, 0.6) 0.5px, transparent 0.5px), radial-gradient(rgba(23, 20, 26, 0.6) 0.5px, transparent 0.5px);
background-size: 3px 3px, 5px 5px;
}
.test-a__quote {
position: relative;
z-index: 1;
margin: 0 0 12px;
color: #ffffff;
font: 600 clamp(12px, 3vw, 13.5px)/1.5 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.test-a__name {
position: relative;
z-index: 1;
margin: 0;
color: #ffffff;
font-weight: 600;
font-size: clamp(10.5px, 2.4vw, 11.5px);
line-height: 1.3;
}
@keyframes floatA {
0%, 100% {
transform: translate(0, 0);
}
50% {
transform: translate(24px, 28px);
}
}
@keyframes floatB {
0%, 100% {
transform: translate(0, 0);
}
50% {
transform: translate(-28px, -20px);
}
}
@media (prefers-reduced-motion: reduce) {
.test-a__orb {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraTestimonial.module.scss";
export type AuroraTestimonialProps = {
quote?: string;
name?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraTestimonial({
quote = "Our launch got twice as fast thanks to this.",
name = "Kim Do-hyun · Product Lead",
duration = 2000,
easing = "steps(4, jump-none)",
color = "#ff4d1f",
}: AuroraTestimonialProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<figure className={styles.wrap} style={vars}>
<div className={`${styles.orb} ${styles.orb1}`} aria-hidden="true" />
<div className={`${styles.orb} ${styles.orb2}`} aria-hidden="true" />
<blockquote className={styles.card}>
<p className={styles.quote}>“{quote}”</p>
<figcaption className={styles.name}>{name}</figcaption>
</blockquote>
</figure>
);
}06 · Blurred-background quote — Measured area 45.86% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password
Keeping the text crisp
The glass in front only gets a light blur so the text stays crisp while the color behind it stays soft.
.test-a__orb {
position: absolute; width: 50%; aspect-ratio: 1;
border-radius: 50%; filter: blur(20px);
}
.test-a__orb--1 {
background: radial-gradient($subject-blue 0%, $subject-blue 45%, transparent 72%);
top: -8%; left: -8%;
animation: floatA $duration steps(4, jump-none) infinite;
}
.test-a__card {
background: rgba($stage-ink, .22);
backdrop-filter: blur(4px);
border: 1px solid rgba($stage-paper, .18);
}
07CTA band
The band is the subject
A single full-width band carries a five-color aurora that keeps flowing, with a static grain layer on top so the screen reads like a printed poster.

<main class="stage">
<section class="cta-a">
<h3 class="cta-a__title">Start right now</h3>
<button class="cta-a__btn" type="button">Start for free</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: linear;
$color: $subject-blue;
@include stage($stage-orange);
.stage { padding: clamp(8px, 4vw, 24px); }
// 오로라 그라디언트가 화면 폭 전체를 흐르고, 그 위에 그레인이 얹힌다
.cta-a {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: clamp(260px, 90vw, 420px);
border-radius: $r-card;
padding: $sp-8 $sp-6;
background: linear-gradient(120deg, $stage-ink, $color, $stage-orange, $stage-yellow, $stage-ink);
background-size: 300% 100%;
animation: flow $duration $easing infinite;
}
.cta-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .3;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($stage-paper, .6) .5px, transparent .5px),
radial-gradient(rgba($stage-ink, .6) .5px, transparent .5px);
background-size: 3px 3px, 5px 5px;
}
.cta-a__title { position: relative; z-index: 1; margin: 0 0 $sp-4; color: $stage-paper; font: 700 clamp(16px, 4vw, 20px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; }
.cta-a__btn { position: relative; z-index: 1; border: 0; border-radius: $r-pill; background: $stage-paper; color: $stage-ink; padding: $sp-2 $sp-6; font-weight: 700; font-size: clamp(11px, 2.6vw, 13px); line-height: 1; }
@keyframes flow { 0% { background-position: 0% 50%; } 100% { background-position: 100% 50%; } }
@media (prefers-reduced-motion: reduce) {
.cta-a { animation: none; background-position: 0% 50%; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
padding: clamp(8px, 4vw, 24px);
}
.cta-a {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: clamp(260px, 90vw, 420px);
border-radius: 16px;
padding: 32px 24px;
background: linear-gradient(120deg, #17141a, #2f6df6, #ff4d1f, #ffd23f, #17141a);
background-size: 300% 100%;
animation: flow 2s linear infinite;
}
.cta-a::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.3;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 255, 255, 0.6) 0.5px, transparent 0.5px), radial-gradient(rgba(23, 20, 26, 0.6) 0.5px, transparent 0.5px);
background-size: 3px 3px, 5px 5px;
}
.cta-a__title {
position: relative;
z-index: 1;
margin: 0 0 16px;
color: #ffffff;
font: 700 clamp(16px, 4vw, 20px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.cta-a__btn {
position: relative;
z-index: 1;
border: 0;
border-radius: 999px;
background: #ffffff;
color: #17141a;
padding: 8px 24px;
font-weight: 700;
font-size: clamp(11px, 2.6vw, 13px);
line-height: 1;
}
@keyframes flow {
0% {
background-position: 0% 50%;
}
100% {
background-position: 100% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.cta-a {
animation: none;
background-position: 0% 50%;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraCtaBand.module.scss";
export type AuroraCtaBandProps = {
title?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraCtaBand({
title = "Start right now",
cta = "Start for free",
duration = 2000,
easing = "linear",
color = "#2f6df6",
}: AuroraCtaBandProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.band} style={vars}>
<h3 className={styles.title}>{title}</h3>
<button type="button" className={styles.btn}>{cta}</button>
</section>
);
}07 · Full-bleed aurora band — Measured area 36.246% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password
Third largest in the set
The whole band is the moving subject here, which puts its changed-pixel area among the largest in the set — third out of nine.
.cta-a {
background: linear-gradient(120deg,
$stage-ink, $color, $stage-orange, $stage-yellow, $stage-ink);
background-size: 300% 100%;
animation: flow $duration linear infinite;
}
@keyframes flow {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
08Footer
What makes a grainy gradient texture flicker
The film grain jumps to a new spot every frame instead of drifting smoothly, which is what actually reads as grain.

<main class="stage">
<footer class="footer-a">
<div class="footer-a__col">
<p class="footer-a__label">Product</p>
<p class="footer-a__link">Features</p>
</div>
<div class="footer-a__col">
<p class="footer-a__label">Company</p>
<p class="footer-a__link">About</p>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: steps(6, jump-none); // 그레인이 부드럽게 흐르지 않고 프레임마다 뚝뚝 튄다
$color: $subject-cream;
@include stage($stage-paper);
.footer-a {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: clamp(260px, 88vw, 400px);
border-radius: $r-card;
padding: $sp-6;
background: $stage-ink;
display: grid;
grid-template-columns: 1fr 1fr;
gap: $sp-4;
}
// 필름 그레인 — steps() 로 프레임마다 자리를 바꿔 지지직거리는 질감을 만든다 (이 항목의 유일한 애니메이션)
.footer-a::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: .45;
mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($color, .7) .6px, transparent .6px),
radial-gradient(rgba($stage-ink, .7) .6px, transparent .6px);
background-size: 3px 3px, 5px 5px;
animation: grainJump $duration $easing infinite;
}
.footer-a__col { position: relative; z-index: 1; border-radius: $r-sm; background: rgba($stage-paper, .05); padding: $sp-3; display: flex; flex-direction: column; gap: $sp-1; }
.footer-a__label { margin: 0; color: rgba($stage-paper, .5); font: 700 clamp(9.5px, 2.2vw, 10.5px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif; text-transform: uppercase; letter-spacing: .06em; }
.footer-a__link { margin: 0; color: $stage-paper; font-weight: 600; font-size: clamp(11px, 2.6vw, 12px); line-height: 1.4; }
@keyframes grainJump {
0% { background-position: 0 0, 0 0; }
20% { background-position: -6px 4px, 3px -5px; }
40% { background-position: 5px -3px, -4px 6px; }
60% { background-position: -3px -6px, 6px 2px; }
80% { background-position: 4px 5px, -5px -3px; }
100% { background-position: 0 0, 0 0; }
}
@media (prefers-reduced-motion: reduce) {
.footer-a::before { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.footer-a {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: clamp(260px, 88vw, 400px);
border-radius: 16px;
padding: 24px;
background: #17141a;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.footer-a::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.45;
mix-blend-mode: overlay;
background-image: radial-gradient(rgba(255, 247, 230, 0.7) 0.6px, transparent 0.6px), radial-gradient(rgba(23, 20, 26, 0.7) 0.6px, transparent 0.6px);
background-size: 3px 3px, 5px 5px;
animation: grainJump 2s steps(6, jump-none) infinite;
}
.footer-a__col {
position: relative;
z-index: 1;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
padding: 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.footer-a__label {
margin: 0;
color: rgba(255, 255, 255, 0.5);
font: 700 clamp(9.5px, 2.2vw, 10.5px)/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.footer-a__link {
margin: 0;
color: #ffffff;
font-weight: 600;
font-size: clamp(11px, 2.6vw, 12px);
line-height: 1.4;
}
@keyframes grainJump {
0% {
background-position: 0 0, 0 0;
}
20% {
background-position: -6px 4px, 3px -5px;
}
40% {
background-position: 5px -3px, -4px 6px;
}
60% {
background-position: -3px -6px, 6px 2px;
}
80% {
background-position: 4px 5px, -5px -3px;
}
100% {
background-position: 0 0, 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.footer-a::before {
animation: none;
}
}import type { CSSProperties } from "react";
import styles from "./AuroraFooter.module.scss";
export type AuroraFooterProps = {
columns?: { label: string; link: string }[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraFooter({
columns = [
{ label: "Product", link: "Features" },
{ label: "Company", link: "About" },
],
duration = 2000,
easing = "steps(6, jump-none)",
color = "#fff7e6",
}: AuroraFooterProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<footer className={styles.footer} style={vars}>
{columns.map((col) => (
<div key={col.label} className={styles.col}>
<p className={styles.label}>{col.label}</p>
<p className={styles.link}>{col.link}</p>
</div>
))}
</footer>
);
}08 · Grainy dark footer — Measured area 0.78% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password
Why steps() is the trick
Swapping the timing function for steps() is the whole trick — it's the only section where the grain itself is the animation.
.footer-a::before {
content: ""; position: absolute; inset: 0;
opacity: .45; mix-blend-mode: overlay;
background-image:
radial-gradient(rgba($color, .7) .6px, transparent .6px),
radial-gradient(rgba($stage-ink, .7) .6px, transparent .6px);
background-size: 3px 3px, 5px 5px;
animation: grainJump $duration steps(6, jump-none) infinite;
}
@keyframes grainJump {
0% { background-position: 0 0, 0 0; }
40% { background-position: 5px -3px, -4px 6px; }
100% { background-position: 0 0, 0 0; }
}
09Buttons & inputs
A gradient that waits for hover
The gradient inside the button only flows on hover, while the input skips a focus ring in favor of a constant, soft double box-shadow glow.

<main class="stage">
<form class="bi-a is-demo">
<input class="bi-a__input" type="email" placeholder="Email address" readonly>
<button class="bi-a__btn" type="button">Sign up</button>
</form>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-out;
$color: $stage-yellow;
@include stage($stage-ink);
.bi-a { display: flex; flex-direction: column; gap: $sp-4; width: clamp(220px, 78vw, 320px); }
.bi-a__input {
border: 0;
border-radius: $r-control;
padding: $sp-3 $sp-4;
background: rgba($stage-paper, .06);
color: $stage-paper;
font: 500 clamp(12px, 3vw, 13.5px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
box-shadow: 0 0 0 2px rgba($subject-blue, .55), 0 0 16px rgba($stage-orange, .45);
}
.bi-a__input::placeholder { color: rgba($stage-paper, .4); }
// 버튼 안 그라디언트는 호버(.is-demo 로 강제 발동)에만 흐른다 — background-position 만 바뀐다
.bi-a__btn {
align-self: flex-start;
border: 0;
border-radius: $r-control;
padding: $sp-3 $sp-6;
color: $stage-ink;
font-weight: 700; font-size: clamp(12px, 3vw, 13.5px); line-height: 1;
background: linear-gradient(120deg, $color, $stage-orange, $subject-blue, $color);
background-size: 260% 100%;
background-position: 0% 50%;
}
.is-demo .bi-a__btn,
.bi-a__btn:hover,
.bi-a__btn:focus-visible {
animation: shift $duration $easing infinite;
}
@keyframes shift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
@media (prefers-reduced-motion: reduce) {
.is-demo .bi-a__btn, .bi-a__btn:hover, .bi-a__btn:focus-visible { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.bi-a {
display: flex;
flex-direction: column;
gap: 16px;
width: clamp(220px, 78vw, 320px);
}
.bi-a__input {
border: 0;
border-radius: 12px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.06);
color: #ffffff;
font: 500 clamp(12px, 3vw, 13.5px)/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
box-shadow: 0 0 0 2px rgba(47, 109, 246, 0.55), 0 0 16px rgba(255, 77, 31, 0.45);
}
.bi-a__input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.bi-a__btn {
align-self: flex-start;
border: 0;
border-radius: 12px;
padding: 12px 24px;
color: #17141a;
font-weight: 700;
font-size: clamp(12px, 3vw, 13.5px);
line-height: 1;
background: linear-gradient(120deg, #ffd23f, #ff4d1f, #2f6df6, #ffd23f);
background-size: 260% 100%;
background-position: 0% 50%;
}
.is-demo .bi-a__btn,
.bi-a__btn:hover,
.bi-a__btn:focus-visible {
animation: shift 2s ease-out infinite;
}
@keyframes shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .bi-a__btn, .bi-a__btn:hover, .bi-a__btn:focus-visible {
animation: none;
}
}import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./AuroraButtonsInputs.module.scss";
export type AuroraButtonsInputsProps = {
placeholder?: string;
cta?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function AuroraButtonsInputs({
placeholder = "Email address",
cta = "Sign up",
duration = 2000,
easing = "ease-out",
color = "#ffd23f",
}: AuroraButtonsInputsProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const [hover, setHover] = useState(false);
return (
<form className={styles.form} style={vars} onSubmit={(e: { preventDefault: () => void }) => e.preventDefault()}>
<input className={styles.input} type="email" placeholder={placeholder} />
<button
type="submit"
className={`${styles.btn} ${hover ? styles.active : ""}`}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{cta}
</button>
</form>
);
}09 · Gradient buttons, glow focus — Measured area 2.579% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password
Forcing it to play for capture
This is the only section triggered by hover, so the is-demo class forces it to play for capture.
.bi-a__btn {
background: linear-gradient(120deg, $color, $stage-orange, $subject-blue, $color);
background-size: 260% 100%;
}
.bi-a__btn:hover, .bi-a__btn:focus-visible {
animation: shift $duration ease-out infinite;
}
.bi-a__input {
box-shadow: 0 0 0 2px rgba($subject-blue, .55), 0 0 16px rgba($stage-orange, .45);
}
Custom tailwind.config
One config for the whole mesh gradient tailwind css set
Every color and radius that the nine sections reuse lives in this one config, so swapping the three aurora hues here carries across all of them — with two exceptions worth knowing before you touch it.
Two values that live elsewhere
Grain strength isn't in this file at all; it's a hardcoded opacity inside the one-page build's own <style> block. And the bg-aurora gradient utility repeats the same three hex values as literals rather than pulling from colors above it, so changing a color here won't retint that one utility on its own — you'd edit both places.
theme: {
extend: {
colors: {
ink: "#17141a", cream: "#fff7e6", paper: "#ffffff",
blue: "#2f6df6", yellow: "#ffd23f", orange: "#ff4d1f",
},
borderRadius: { tile: "16px", control: "12px", pill: "999px" },
backgroundImage: {
aurora: "linear-gradient(120deg, #17141a, #2f6df6, #ff4d1f, #ffd23f, #17141a)",
},
},
},
Vibe-coding prompt
A fragment is enough
Drop the config above into your project, then hand Claude Code or Cursor the fragment below — it's enough on its own. The full prompt lives in prompt.md inside the zip.
Background is ink only. Only where you want emphasis, overlap 2-3 blurred
(40-60px) blue/yellow/orange blobs so they bleed like an aurora. Never
paint the whole background with a gradient.
Layer faint grain (a radial-gradient dot pattern, opacity around .3) over
every dark surface. Build it with CSS only, never an image file.
What it produced
Applying this instruction to page/index.html and capturing the full page at a 1280px viewport produced the screenshot below. 
Where it breaks (the trap)
When the motion is too small to register
Moving the aurora blobs with a soft ease curve alone can make an animation invisible in practice — behind a heavy blur, the change between adjacent frames drops too low to register. The first version of section 05's recommended plan only swung opacity from .55 to 1, and the render measurement came back with an intensity of zero; only after widening the range to .3–1 and switching the timing to steps(4, jump-none) did it clear six moved frames. Reaching for semantic tags like <ul> or <figure>/<blockquote> without a second thought is another trap — their default browser margins survive unless you reset them, and at a 320px width that pushes the page taller than the viewport. Sections 04 and 06 hit exactly this, and the scrollbar only disappeared once margin: 0 was added explicitly. The zip holding all nine sections and the full one-page source needs the password u7eepwrp to open, and the button below starts the download.
Contrast on top of grain
The last trap is contrast. Desaturating the aurora blobs or switching to a light background makes text sitting on top of the grain lose contrast fast, so any text layer is best kept on the fixed dark ink background rather than directly on the gradient. The moment a grainy field has to become a flat image is already daily life in the marketing email templates, where backgrounds can only be images.
Accessibility
What reduced motion shows instead
Every animated section, including the gradient flow on the section 09 button, is wrapped in the media query defined by the MDN prefers-reduced-motion documentation, so a visitor with reduced motion turned on sees a static version of each one instead.
The older Safari fallback
Section 03's mask-composite border follows the MDN mask-composite documentation and keeps the older Safari fallback -webkit-mask-composite: xor alongside the standard value.
Text never sits on the gradient
Every quote and button label is set as white text on ink or ink text on cream, never directly on the moving gradient, so contrast holds up while the colors are shifting. If this particular look isn't the one you need, the design trends category collects the rest, and the about page explains what this site covers overall.
FAQ
Can mesh gradient CSS be built with CSS alone?
Yes — all nine sections in this set ship with zero lines of JavaScript. Even the hover response on the section 09 button runs entirely on the :hover and :focus-visible pseudo-classes; the only script involved is a short one that forces the animation to play while capturing the demo.
Does a grainy gradient css layer require an image file?
No. The grain here is two tiny radial-gradient dots layered and tiled at a 3–5px background size, so there isn't a single image request anywhere. Nudging the dot position every frame, as section 08's footer does, is enough to fake the flicker of real film grain with CSS alone.
Does this only work on a dark background?
The aurora blobs themselves still work on a light background, but all nine sections here default to a dark ink base. On a light background, lower the blob opacity and swap the grain's blend mode from overlay to multiply to keep a similar amount of texture. Push the same light toward metal and it becomes the mirrored surfaces of the Y2K chrome holographic landing.









