Updated 2026-10-14. 12 effects.
An animated gradient background shifts soft colour fields slowly behind your content. The smoothest way is to move a few large blurred colour shapes with transforms, or to animate registered @property colour stops, then add fine grain so the blend never shows bands. Each version here comes as HTML and CSS and as React.
Colour does the work in this collection, so choose the palette before the motion. Warm dawn and paper tones flatter food, craft and hospitality sites; deep teal and ink suit finance and software; pale pearl and duotone washes stay out of the way of busy layouts.
Most of these are CSS. A few use a small canvas because a true mesh gradient, where colour points drift independently, is hard to fake with stacked radial gradients. The canvas ones render at reduced resolution and scale up, which is invisible on a soft gradient and saves a lot of work per frame.
| Effect | Built with | Size (gzipped) | Running cost |
|---|---|---|---|
| Northern Lights Curtain Aurora | CSS only | 3.5 KB | Three blurred curtain layers move by transform on the compositor and one observer pauses them off screen; no frame loop. |
| Dawn Sky Gradient That Slowly Brightens | CSS only | 2.3 KB | Animating registered colour properties repaints one full-size gradient each frame, which is light work but not compositor-only. |
| Deep Sea Teal Mesh Gradient | Canvas 2D | 2.2 KB | One 120 pixel wide canvas recomputed each frame with six colour points, well under a millisecond, paused off screen. |
| Ember Glow Rising Mesh | Canvas 2D | 2.3 KB | Seven colour points on a 110 pixel wide canvas, well under a millisecond a frame, with the loop stopped off screen. |
| Film Colour Wash With Grain | CSS only | 2 KB | Four layers animate transform and opacity only; the grain is a single small tile, so the cost is compositing. |
| Heat Shimmer Desert Gradient | CSS only | 2.2 KB | One SVG displacement filter re-renders the horizon band each frame, a moderate paint cost kept to that strip and paused off screen. |
| Ink Cloud Blooming in Water | Canvas 2D | 2.8 KB | A 140 pixel wide canvas recomputes five blooms per frame, around a millisecond, and the loop stops off screen. |
| Iridescent Pearl Sheen | CSS only | 2 KB | Two full-size conic gradients repaint as their angle turns, plus two transformed highlights; modest paint cost, no script loop. |
| Sea Glass Radial Gradient Stack | CSS only | 2 KB | One stacked gradient repaints as its pool positions animate, plus one transformed sheen; light paint work, no script per frame. |
| Soft Spotlight That Follows the Pointer | CSS only | 2.4 KB | A tiny script eases two transformed layers each frame; painting stays on the compositor and the loop stops off screen. |
| Sunset Haze Over Warm Paper | CSS only | 2.1 KB | Five soft layers drift by transform only and the textures are static tiles, so the browser composites rather than repaints. |
| Two-Tone Duotone Drift | CSS only | 1.9 KB | One gradient repaints as its angle and midpoint animate, and two pools move by transform; light, with no script per frame. |
A night sky over a dark spruce ridge where three translucent curtains of green light lean, stretch and sway. Each curtain is a vertical colour fade cut into uneven rays by a CSS mask, so it reads as hanging light rather than a blurry blob, and its top edge fades into a dusky rose the way real auroras do. A film grain layer keeps the dark blues smooth. On phones the ridge stays pinned to the bottom edge while the curtains scale with the section.
<section class="nlc-hero">
<div class="nlc-sky" aria-hidden="true">
<div class="nlc-stars"></div>
<div class="nlc-curtain nlc-c1"><div class="nlc-rays"></div></div>
<div class="nlc-curtain nlc-c2"><div class="nlc-rays"></div></div>
<div class="nlc-curtain nlc-c3"><div class="nlc-rays"></div></div>
<div class="nlc-glow"></div>
<svg class="nlc-ridge" viewBox="0 0 1440 120" preserveAspectRatio="xMidYMax slice"><path d="M0 120L0 92L0 96L9 61L19 96L15 97L23 61L32 97L27 90L36 67L44 90L38 92L49 57L59 92L54 95L64 35L74 95L67 96L77 69L87 96L83 96L92 48L101 96L94 94L102 61L110 94L107 91L117 52L127 91L121 91L131 45L142 91L135 91L144 34L152 91L146 91L155 54L165 91L161 92L170 38L180 92L176 93L187 50L198 93L194 89L203 62L213 89L210 97L220 65L229 97L225 91L231 66L237 91L233 96L243 66L254 96L249 90L259 45L269 90L263 89L273 54L283 89L276 93L283 68L291 93L288 94L296 69L304 94L301 92L309 36L318 92L315 90L325 63L335 90L332 92L339 59L345 92L341 94L347 74L354 94L349 94L355 77L362 94L357 95L364 45L370 95L367 89L377 61L387 89L382 94L389 63L395 94L391 89L397 46L404 89L401 93L407 68L413 93L412 94L420 45L427 94L422 94L432 50L442 94L435 91L446 41L456 91L454 94L462 55L471 94L464 91L471 74L479 91L478 89L489 53L500 89L494 95L501 73L508 95L505 92L514 44L522 92L520 91L530 68L541 91L538 94L548 66L558 94L556 89L564 46L572 89L569 90L576 71L583 90L581 92L592 67L603 92L597 89L603 61L609 89L607 90L615 55L623 90L622 95L629 75L637 95L634 90L640 67L647 90L643 94L654 64L664 94L663 97L671 67L680 97L676 96L686 80L696 96L691 93L699 59L706 93L703 95L712 64L721 95L715 91L724 57L733 91L732 93L740 65L749 93L746 89L754 62L763 89L760 93L767 50L775 93L774 93L780 60L787 93L782 91L791 72L801 91L799 96L808 73L818 96L816 94L827 55L837 94L832 94L839 47L846 94L843 91L851 68L858 91L852 94L858 63L864 94L862 91L873 64L884 91L883 91L889 72L895 91L891 90L902 70L913 90L906 91L915 65L924 91L917 96L925 74L933 96L932 90L939 50L945 90L940 93L948 49L956 93L955 95L963 72L972 95L966 95L973 75L980 95L975 96L984 56L992 96L988 91L994 74L1000 91L997 96L1008 71L1019 96L1016 94L1026 61L1037 94L1032 90L1040 44L1048 90L1045 97L1053 57L1061 97L1055 96L1062 71L1070 96L1064 95L1074 44L1083 95L1077 93L1084 64L1091 93L1087 95L1095 38L1104 95L1103 94L1109 64L1115 94L1112 97L1121 62L1129 97L1124 97L1130 74L1136 97L1133 91L1141 61L1150 91L1147 94L1155 45L1163 94L1162 97L1171 68L1180 97L1178 91L1188 36L1198 91L1191 91L1201 50L1211 91L1209 91L1219 44L1228 91L1222 96L1230 76L1238 96L1236 92L1245 47L1254 92L1250 93L1260 67L1269 93L1265 95L1275 52L1285 95L1277 91L1284 56L1291 91L1291 92L1299 51L1307 92L1305 96L1312 47L1318 96L1314 97L1323 47L1332 97L1325 92L1334 56L1344 92L1338 96L1347 52L1355 96L1354 97L1364 60L1375 97L1370 95L1378 33L1386 95L1381 96L1390 36L1399 96L1395 93L1405 33L1415 93L1413 93L1424 42L1434 93L1426 95L1434 70L1442 95L1437 97L1447 59L1457 97L1440 94L1440 120Z" fill="#03070b"/></svg>
<div class="nlc-grain"></div>
</div>
<div class="nlc-content">
<p class="nlc-kicker">Kettle Lake Cabins · 68° north</p>
<h1 class="nlc-title">Cabins far enough north to watch the sky move.</h1>
<p class="nlc-copy">Six timber cabins on a frozen lake, a sauna by the shore and a guide who knocks on your door when the lights come out.</p>
<a class="nlc-button" href="#">Check winter dates</a>
</div>
</section>
<style>
/* Change these first:
--nlc-green: main curtain colour (keep it soft, not neon)
--nlc-rose: the faint colour at the top of the tallest curtain
--nlc-speed: one sway cycle; larger is calmer
.nlc-grain opacity: film grain strength */
.nlc-hero { --nlc-green: 110, 214, 160; --nlc-rose: 168, 120, 176; --nlc-speed: 22s;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 640px;
background: linear-gradient(180deg, #04080f 0%, #08121d 50%, #0d1c26 100%); font-family: ui-sans-serif, system-ui, sans-serif; }
.nlc-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.nlc-stars { position: absolute; inset: 0 0 30% 0; opacity: .55;
background-image: radial-gradient(1px 1px at 12% 18%, #fff 50%, transparent 51%), radial-gradient(1px 1px at 27% 8%, #dfe8f5 50%, transparent 51%), radial-gradient(1.4px 1.4px at 44% 22%, #fff 50%, transparent 51%), radial-gradient(1px 1px at 63% 12%, #e9eef8 50%, transparent 51%), radial-gradient(1px 1px at 78% 28%, #fff 50%, transparent 51%), radial-gradient(1.2px 1.2px at 91% 9%, #fff 50%, transparent 51%), radial-gradient(1px 1px at 6% 40%, #cfd9ea 50%, transparent 51%), radial-gradient(1px 1px at 55% 36%, #fff 50%, transparent 51%), radial-gradient(1px 1px at 86% 44%, #dbe4f2 50%, transparent 51%), radial-gradient(1px 1px at 35% 48%, #fff 50%, transparent 51%); }
.nlc-curtain { position: absolute; left: -25%; width: 150%; mix-blend-mode: screen; transform-origin: 50% 100%;
-webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 45%, #000 70%, transparent 100%), linear-gradient(90deg, transparent 4%, #000 30%, #000 70%, transparent 96%);
-webkit-mask-composite: source-in; mask-image: linear-gradient(180deg, transparent 0%, #000 45%, #000 70%, transparent 100%), linear-gradient(90deg, transparent 4%, #000 30%, #000 70%, transparent 96%); mask-composite: intersect;
animation: nlc-sway var(--nlc-speed) ease-in-out infinite alternate; }
.nlc-rays { position: absolute; inset: 0 -320px 0 0; filter: blur(5px);
background: linear-gradient(0deg, rgba(var(--nlc-green), 0) 0%, rgba(var(--nlc-green), .85) 26%, rgba(var(--nlc-green), .32) 52%, rgba(var(--nlc-rose), .22) 76%, rgba(var(--nlc-rose), 0) 100%);
-webkit-mask-image: repeating-linear-gradient(90deg, rgba(0,0,0,.18) 0px, #000 9px, rgba(0,0,0,.5) 16px, rgba(0,0,0,.1) 27px, #000 41px, rgba(0,0,0,.35) 52px, rgba(0,0,0,.75) 66px, rgba(0,0,0,.12) 80px);
mask-image: repeating-linear-gradient(90deg, rgba(0,0,0,.18) 0px, #000 9px, rgba(0,0,0,.5) 16px, rgba(0,0,0,.1) 27px, #000 41px, rgba(0,0,0,.35) 52px, rgba(0,0,0,.75) 66px, rgba(0,0,0,.12) 80px);
animation: nlc-shimmer 40s linear infinite; }
.nlc-c1 { top: 4%; height: 62%; opacity: .9; rotate: -7deg; animation-delay: -9s; }
.nlc-c2 { top: 14%; height: 50%; opacity: .55; rotate: 5deg; animation-duration: calc(var(--nlc-speed) * 1.3); animation-delay: -4s; }
.nlc-c2 .nlc-rays { animation-duration: 55s; animation-direction: reverse; }
.nlc-c3 { top: -6%; height: 58%; left: 10%; width: 110%; opacity: .4; rotate: -16deg; animation-duration: calc(var(--nlc-speed) * .8); animation-delay: -15s; }
.nlc-glow { position: absolute; left: 0; right: 0; bottom: 0; height: 45%; background: radial-gradient(70% 90% at 40% 100%, rgba(var(--nlc-green), .16), transparent 70%); }
.nlc-ridge { position: absolute; left: 0; bottom: 0; width: 100%; height: 110px; }
.nlc-grain { position: absolute; inset: 0; opacity: .22; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.nlc-content { box-sizing: border-box; position: relative; width: 100%; max-width: 64rem; margin: 0 auto; padding: 120px 24px 150px; }
.nlc-kicker { margin: 0; font-size: 14px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: #b9e8cf; }
.nlc-title { margin: 18px 0 0; max-width: 44rem; font-size: clamp(36px, 6vw, 62px); line-height: 1.1; font-weight: 600; color: #fff; text-shadow: 0 2px 24px rgba(3, 8, 14, .5); }
.nlc-copy { margin: 22px 0 0; max-width: 34rem; font-size: 18px; line-height: 1.65; color: #dbe7ee; }
.nlc-button { display: inline-flex; align-items: center; height: 48px; margin-top: 36px; padding: 0 26px; border-radius: 10px; background: #e9f6ef; color: #0b2a20; font-size: 15px; font-weight: 600; text-decoration: none; }
.nlc-button:hover { background: #fff; }
.nlc-button:focus-visible { outline: 2px solid #b9e8cf; outline-offset: 3px; }
.nlc-hero[data-nlc-paused] * { animation-play-state: paused !important; }
@keyframes nlc-sway { 0% { transform: translateX(-5%) skewX(-10deg) scaleY(.94); } 100% { transform: translateX(5%) skewX(8deg) scaleY(1.06); } }
@keyframes nlc-shimmer { to { transform: translateX(-320px); } }
@media (min-width: 640px) { .nlc-content { padding: 120px 40px 160px; } }
@media (prefers-reduced-motion: reduce) { .nlc-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the sway while the hero is scrolled out of view.
var root = document.querySelector(".nlc-hero:not([data-nlc-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-nlc-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-nlc-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The curtains stop in a middle pose over the ridge, so the still sky still shows a full aurora.
Running cost: Three blurred curtain layers move by transform on the compositor and one observer pauses them off screen; no frame loop.
Full page for Northern Lights Curtain Aurora
A harbour sky moves from blue hour to early sunrise and back over half a minute. The gradient stops are registered CSS colour properties, so the browser blends them smoothly instead of jumping, while a low sun lifts behind the horizon and lays a striped glint on the water. Fits early morning brands, rowing or running clubs, cafes and anything sold on a fresh start.
<section class="dwn-hero">
<div class="dwn-sky" aria-hidden="true">
<div class="dwn-cloud dwn-cloud-a"></div>
<div class="dwn-cloud dwn-cloud-b"></div>
<div class="dwn-sun"></div>
<div class="dwn-disc"></div>
<div class="dwn-water"></div>
<div class="dwn-shine"></div>
<div class="dwn-grain"></div>
</div>
<div class="dwn-content">
<p class="dwn-kicker">Harbour Dawn Rowing Club</p>
<h1 class="dwn-title">On the water before the city wakes up.</h1>
<p class="dwn-copy">Coached sessions at first light, four days a week. Boats, oars and a hot drink on the pontoon are included.</p>
<a class="dwn-button" href="#">Join a 6am session</a>
</div>
</section>
<style>
/* Change these first:
the three colour sets in @keyframes dwn-rise (night, early, risen)
--dwn-speed: how long one brightening takes; it then eases back
.dwn-grain opacity: grain strength */
@property --dwn-top { syntax: "<color>"; inherits: true; initial-value: #1b2a55; }
@property --dwn-mid { syntax: "<color>"; inherits: true; initial-value: #66577f; }
@property --dwn-low { syntax: "<color>"; inherits: true; initial-value: #e09678; }
@property --dwn-sea { syntax: "<color>"; inherits: true; initial-value: #2c2f4c; }
.dwn-hero { --dwn-speed: 32s; --dwn-top: #1b2a55; --dwn-mid: #66577f; --dwn-low: #e09678; --dwn-sea: #2c2f4c;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: flex-start; min-height: 620px;
background: linear-gradient(180deg, var(--dwn-top) 0%, var(--dwn-mid) 50%, var(--dwn-low) 76%);
font-family: ui-sans-serif, system-ui, sans-serif;
animation: dwn-rise var(--dwn-speed) ease-in-out -11s infinite alternate; }
.dwn-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.dwn-sun { position: absolute; left: 62%; top: 76%; width: 70vmax; height: 70vmax; max-width: 980px; max-height: 980px; border-radius: 50%;
background: radial-gradient(closest-side, rgba(255, 232, 196, 1), rgba(255, 196, 146, .55) 18%, rgba(255, 170, 130, .12) 52%, transparent 75%);
transform: translate(-50%, -38%); animation: dwn-sun var(--dwn-speed) ease-in-out -11s infinite alternate; }
.dwn-water { position: absolute; left: 0; right: 0; top: 76%; bottom: 0; background: linear-gradient(180deg, var(--dwn-low) 0%, var(--dwn-sea) 30%, #151827 100%); }
.dwn-shine { position: absolute; left: 62%; top: 76%; width: 46%; height: 22%; transform: translateX(-50%);
background: repeating-linear-gradient(180deg, rgba(255, 222, 186, .5) 0 2px, transparent 3px 7px);
-webkit-mask-image: radial-gradient(50% 100% at 50% 0%, #000 0%, rgba(0,0,0,.35) 45%, transparent 100%); mask-image: radial-gradient(50% 100% at 50% 0%, #000 0%, rgba(0,0,0,.35) 45%, transparent 100%);
animation: dwn-sun var(--dwn-speed) ease-in-out -11s infinite alternate; animation-name: dwn-glint; }
.dwn-disc { position: absolute; left: 62%; top: 76%; width: 64px; height: 64px; margin: -32px 0 0 -32px; border-radius: 50%;
background: radial-gradient(closest-side, #fff4e2 60%, rgba(255, 228, 190, .6) 80%, transparent);
animation: dwn-disc var(--dwn-speed) ease-in-out -11s infinite alternate; }
.dwn-cloud { position: absolute; border-radius: 50%; background: radial-gradient(closest-side, rgba(250, 196, 170, .38), rgba(250, 196, 170, 0)); }
.dwn-cloud-a { left: -10%; top: 54%; width: 70%; height: 40px; animation: dwn-drift 70s linear -20s infinite alternate; }
.dwn-cloud-b { right: -15%; top: 63%; width: 60%; height: 26px; opacity: .8; animation: dwn-drift 90s linear -50s infinite alternate-reverse; }
.dwn-grain { position: absolute; inset: 0; opacity: .3; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.dwn-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 104px 24px 220px; }
.dwn-kicker { margin: 0; font-size: 14px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: #f3dccb; }
.dwn-title { margin: 16px 0 0; max-width: 40rem; font-size: clamp(36px, 6vw, 60px); line-height: 1.1; font-weight: 600; color: #fff; text-shadow: 0 1px 18px rgba(20, 24, 50, .35); }
.dwn-copy { margin: 20px 0 0; max-width: 32rem; font-size: 18px; line-height: 1.65; color: #f1ecf4; text-shadow: 0 1px 12px rgba(20, 24, 50, .35); }
.dwn-button { display: inline-flex; align-items: center; height: 48px; margin-top: 34px; padding: 0 26px; border-radius: 999px; background: #1b2140; color: #fff; font-size: 15px; font-weight: 600; text-decoration: none; }
.dwn-button:hover { background: #10142a; }
.dwn-button:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.dwn-hero[data-dwn-paused], .dwn-hero[data-dwn-paused] * { animation-play-state: paused !important; }
@keyframes dwn-rise {
0% { --dwn-top: #0d1533; --dwn-mid: #2f2d55; --dwn-low: #8a5a72; --dwn-sea: #1c1d33; }
100% { --dwn-top: #2d4a7e; --dwn-mid: #8f7fa6; --dwn-low: #f2b98e; --dwn-sea: #3f4466; }
}
@keyframes dwn-sun { 0% { transform: translate(-50%, -22%); opacity: .55; } 100% { transform: translate(-50%, -50%); opacity: 1; } }
@keyframes dwn-disc { 0% { transform: translateY(30px); } 100% { transform: translateY(-6px); } }
@keyframes dwn-glint { 0% { opacity: .35; } 100% { opacity: 1; } }
@keyframes dwn-drift { to { transform: translateX(12%); } }
@media (min-width: 640px) { .dwn-content { padding: 120px 40px 230px; } }
@media (prefers-reduced-motion: reduce) { .dwn-hero, .dwn-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the sunrise while the hero is scrolled out of view.
var root = document.querySelector(".dwn-hero:not([data-dwn-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-dwn-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-dwn-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The sky holds a still early-sunrise mix of blue, mauve and peach with the sun resting on the horizon.
Running cost: Animating registered colour properties repaints one full-size gradient each frame, which is light work but not compositor-only.
Six teal and blue-green pools wander slowly through dark water, blending into one soft mesh with no hard edges. The mesh is painted onto a tiny canvas and stretched by the browser, with a grain layer on top so the deep shades never step into bands. It suits diving, marine, wellness or any calm product that wants depth behind white type.
<section class="dsm-hero">
<canvas class="dsm-canvas" aria-hidden="true"></canvas>
<div class="dsm-grain" aria-hidden="true"></div>
<div class="dsm-content">
<p class="dsm-kicker">Kelpline Dive School</p>
<h1 class="dsm-title">Learn to breathe slowly at eighteen metres.</h1>
<p class="dsm-copy">Small groups, warm wetsuits and an instructor who stays within arm's reach from the first giant stride to the last safety stop.</p>
<a class="dsm-button" href="#">Book a try dive</a>
</div>
</section>
<style>
/* Grain strength: .dsm-grain opacity. Page colour before the canvas paints: .dsm-hero background. */
.dsm-hero { position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 600px; background: #031620; font-family: ui-sans-serif, system-ui, sans-serif; }
.dsm-canvas, .dsm-grain { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }
.dsm-grain { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); mix-blend-mode: overlay; opacity: .28; }
.dsm-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 96px 24px; }
.dsm-kicker { margin: 0; font-size: 14px; font-weight: 500; letter-spacing: .2em; text-transform: uppercase; color: rgba(204, 251, 241, .8); }
.dsm-title { margin: 20px 0 0; max-width: 42rem; font-size: clamp(36px, 6vw, 60px); line-height: 1.15; font-weight: 600; color: #fff; }
.dsm-copy { margin: 24px 0 0; max-width: 36rem; font-size: 18px; line-height: 1.65; color: rgba(240, 253, 250, .85); }
.dsm-button { display: inline-flex; align-items: center; height: 48px; margin-top: 40px; padding: 0 28px; border-radius: 999px; background: #e6f4f1; color: #06303a; font-size: 14px; font-weight: 600; text-decoration: none; }
.dsm-button:hover { background: #fff; }
.dsm-button:focus-visible { outline: 2px solid #99f6e4; outline-offset: 3px; }
@media (min-width: 640px) { .dsm-content { padding: 96px 40px; } }
</style>
<script>
(function () {
// Colour pools that drift through the mesh, as [r, g, b].
var POOLS = [[9, 58, 72], [18, 96, 98], [36, 122, 114], [6, 40, 60], [52, 132, 124], [4, 28, 44]];
// Water colour between the pools.
var DEPTH = [3, 22, 32];
// Bigger is slower.
var SLOWNESS = 9000;
var root = document.querySelector(".dsm-hero:not([data-dsm-ready])");
if (!root) return;
root.setAttribute("data-dsm-ready", "");
var canvas = root.querySelector(".dsm-canvas");
var ctx = canvas.getContext("2d");
if (!ctx) return;
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var W = 120, H = 72, aspect = 1.6, img = ctx.createImageData(W, H), raf = 0, onScreen = true;
var start = performance.now() - 26000;
var pools = POOLS.map(function (col, i) {
return { col: col, fx: 0.7 + i * 0.23, fy: 0.9 + i * 0.17, px: i * 1.9, py: i * 2.7, r: 0.34 + (i % 3) * 0.08 };
});
function draw(now) {
var t = (now - start) / SLOWNESS;
var pos = pools.map(function (p) {
return { x: (0.5 + 0.46 * Math.sin(t * p.fx + p.px)) * aspect, y: 0.5 + 0.44 * Math.cos(t * p.fy + p.py), k: 1 / (p.r * p.r), col: p.col };
});
var d = img.data, o = 0;
for (var y = 0; y < H; y++) {
var ny = y / (H - 1), shade = 1 - ny * 0.35;
for (var x = 0; x < W; x++) {
var nx = (x / (W - 1)) * aspect;
var r = DEPTH[0] * 0.35, g = DEPTH[1] * 0.35, b = DEPTH[2] * 0.35, s = 0.35;
for (var i = 0; i < pos.length; i++) {
var p = pos[i], dx = nx - p.x, dy = ny - p.y;
var w = Math.exp(-(dx * dx + dy * dy) * p.k);
r += p.col[0] * w; g += p.col[1] * w; b += p.col[2] * w; s += w;
}
d[o] = (r / s) * shade; d[o + 1] = (g / s) * shade; d[o + 2] = (b / s) * shade; d[o + 3] = 255;
o += 4;
}
}
ctx.putImageData(img, 0, 0);
}
function loop(now) {
draw(now);
if (onScreen && !still) raf = requestAnimationFrame(loop);
}
function resize() {
var box = root.getBoundingClientRect();
aspect = Math.max(0.4, box.width / Math.max(1, box.height));
H = Math.max(24, Math.min(160, Math.round(W / aspect)));
canvas.width = W; canvas.height = H;
img = ctx.createImageData(W, H);
draw(performance.now());
}
new ResizeObserver(resize).observe(root);
new IntersectionObserver(function (entries) {
onScreen = entries[0].isIntersecting;
cancelAnimationFrame(raf);
if (onScreen && !still) raf = requestAnimationFrame(loop);
}).observe(root);
resize();
})();
</script>
With reduced motion: The mesh is painted once and stays as a single settled frame of deep teal water.
Running cost: One 120 pixel wide canvas recomputed each frame with six colour points, well under a millisecond, paused off screen.
Seven warm glows lift slowly from a bed of coals below the frame, sway a little, brighten, then cool and vanish near the top while charcoal fills the gaps. Each glow is a colour point in a mesh that adds light rather than averaging it, so where two meet the heat builds. Made for restaurants, bakeries, fireplace makers, whisky and anything that wants a hearth behind centred type.
<section class="emb-hero">
<canvas class="emb-canvas" aria-hidden="true"></canvas>
<div class="emb-grain" aria-hidden="true"></div>
<div class="emb-content">
<p class="emb-kicker">Coalhouse · Wood-fired kitchen</p>
<h1 class="emb-title">Everything on the menu meets the fire at least once.</h1>
<p class="emb-copy">Oak and cherry wood, a hearth that stays lit from noon, and a counter where you can watch it all happen.</p>
<a class="emb-button" href="#">Book a table</a>
</div>
</section>
<style>
/* Grain strength: .emb-grain opacity. Ember colours, speed and heat live at the top of the script. */
.emb-hero { position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 600px; background: #140d0b; font-family: ui-sans-serif, system-ui, sans-serif; }
.emb-canvas, .emb-grain { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }
.emb-grain { opacity: .3; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.emb-content { box-sizing: border-box; width: 100%; max-width: 48rem; margin: 0 auto; padding: 96px 24px; text-align: center; }
.emb-kicker { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: .22em; text-transform: uppercase; color: #f0b98c; }
.emb-title { margin: 20px 0 0; font-size: clamp(36px, 6vw, 60px); line-height: 1.08; font-weight: 600; letter-spacing: -.015em; color: #fff5ec; }
.emb-copy { margin: 20px auto 0; max-width: 32rem; font-size: 18px; line-height: 1.65; color: #ead8c9; }
.emb-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 28px; border-radius: 999px; background: #f6e3cf; color: #2a120a; font-size: 15px; font-weight: 600; text-decoration: none; }
.emb-button:hover { background: #fff; }
.emb-button:focus-visible { outline: 2px solid #f6e3cf; outline-offset: 3px; }
@media (min-width: 640px) { .emb-content { padding: 96px 40px; } }
</style>
<script>
(function () {
// Ember colours as [r, g, b]; they rise and fade over the charcoal.
var EMBERS = [[168, 64, 24], [196, 98, 36], [120, 36, 18], [210, 128, 52], [150, 52, 22], [184, 82, 30], [132, 44, 20]];
// The charcoal behind everything.
var CHARCOAL = [20, 13, 11];
// Seconds for an ember to rise the full height.
var RISE = 22;
// Overall brightness of the embers (0 to 1).
var HEAT = 0.62;
var root = document.querySelector(".emb-hero:not([data-emb-ready])");
if (!root) return;
root.setAttribute("data-emb-ready", "");
var canvas = root.querySelector(".emb-canvas");
var ctx = canvas.getContext("2d");
if (!ctx) return;
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var W = 110, H = 70, aspect = 1.6, img = ctx.createImageData(W, H), raf = 0, onScreen = true, start = performance.now();
var embers = EMBERS.map(function (col, i) {
return { col: col, lane: 0.08 + ((i * 0.37) % 1) * 0.86, phase: i / EMBERS.length, sway: 0.5 + (i % 3) * 0.3, r: 0.16 + (i % 4) * 0.04 };
});
function draw(t) {
var pos = embers.map(function (e) {
var p = (t / RISE + e.phase) % 1;
var life = Math.sin(Math.PI * Math.min(1, p * 1.15)) * (1 - p * 0.55);
return { x: (e.lane + 0.06 * Math.sin(t * 0.3 * e.sway + e.phase * 9)) * aspect, y: 1.15 - p * 1.35, k: 1 / (e.r * e.r), a: life * HEAT, col: e.col };
});
var d = img.data, o = 0;
for (var y = 0; y < H; y++) {
var ny = y / (H - 1), bed = Math.exp(-(1 - ny) * 3.2) * 0.55;
for (var x = 0; x < W; x++) {
var nx = (x / (W - 1)) * aspect;
var r = CHARCOAL[0] + 150 * bed, g = CHARCOAL[1] + 54 * bed, b = CHARCOAL[2] + 22 * bed;
for (var i = 0; i < pos.length; i++) {
var p = pos[i], dx = nx - p.x, dy = ny - p.y;
var w = p.a * Math.exp(-(dx * dx + dy * dy) * p.k);
r += p.col[0] * w; g += p.col[1] * w; b += p.col[2] * w;
}
d[o] = r; d[o + 1] = g; d[o + 2] = b; d[o + 3] = 255;
o += 4;
}
}
ctx.putImageData(img, 0, 0);
}
function seconds(now) { return (now - start) / 1000 + 9; }
function loop(now) {
draw(seconds(now));
if (onScreen && !still) raf = requestAnimationFrame(loop);
}
function resize() {
var box = root.getBoundingClientRect();
aspect = Math.max(0.4, box.width / Math.max(1, box.height));
H = Math.max(30, Math.min(180, Math.round(W / aspect)));
canvas.width = W; canvas.height = H;
img = ctx.createImageData(W, H);
draw(seconds(performance.now()));
}
new ResizeObserver(resize).observe(root);
new IntersectionObserver(function (entries) {
onScreen = entries[0].isIntersecting;
cancelAnimationFrame(raf);
if (onScreen && !still) raf = requestAnimationFrame(loop);
}).observe(root);
resize();
})();
</script>
With reduced motion: A single frame of glowing embers over the coal bed is painted and left still.
Running cost: Seven colour points on a 110 pixel wide canvas, well under a millisecond a frame, with the loop stopped off screen.
A teal and amber colour grade pans slowly across a near-black frame, a warm light leak breathes at the right edge and a coarse grain flickers on top like a projected print. The grain tile jumps between offsets in steps, the cheapest way to fake moving film stock. Built for photographers, film labs, production companies and music pages that want a cinematic opening without a video file.
<section class="flm-hero">
<div class="flm-sky" aria-hidden="true">
<div class="flm-wash flm-teal"></div>
<div class="flm-wash flm-amber"></div>
<div class="flm-leak"></div>
<div class="flm-vignette"></div>
<div class="flm-grain"></div>
</div>
<div class="flm-content">
<p class="flm-kicker">Northlight Film Lab</p>
<h1 class="flm-title">Your rolls, developed and scanned within the week.</h1>
<p class="flm-copy">C-41, black and white and slide film, processed in small batches and scanned at full resolution by people who still shoot.</p>
<a class="flm-button" href="#">Post us your film</a>
</div>
</section>
<style>
/* Change these first:
--flm-shadow and --flm-light: the two halves of the colour grade (r, g, b)
--flm-leak: the warm light leak (r, g, b)
.flm-grain opacity: how much film grain shows */
.flm-hero { --flm-shadow: 34, 86, 92; --flm-light: 196, 118, 52; --flm-leak: 232, 92, 48;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: flex-end; min-height: 620px;
background: #15110f; font-family: ui-sans-serif, system-ui, sans-serif; }
.flm-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.flm-wash { position: absolute; border-radius: 50%; }
.flm-teal { left: -30%; top: -40%; width: 100%; height: 150%; background: radial-gradient(closest-side, rgba(var(--flm-shadow), .95), rgba(var(--flm-shadow), .35) 60%, transparent); animation: flm-pan 24s ease-in-out -7s infinite alternate; }
.flm-amber { right: -35%; top: -30%; width: 95%; height: 140%; background: radial-gradient(closest-side, rgba(var(--flm-light), .78), rgba(var(--flm-light), .25) 60%, transparent); animation: flm-pan 30s ease-in-out -19s infinite alternate-reverse; }
.flm-leak { position: absolute; right: -12%; top: -10%; width: 45%; height: 120%; border-radius: 50%;
background: radial-gradient(closest-side, rgba(var(--flm-leak), .55), rgba(var(--flm-leak), .14) 55%, transparent);
mix-blend-mode: screen; animation: flm-leak 9s ease-in-out -2s infinite; }
.flm-vignette { position: absolute; inset: 0; background: radial-gradient(120% 90% at 50% 45%, transparent 45%, rgba(8, 6, 5, .75) 100%); }
.flm-grain { position: absolute; inset: -60px; opacity: .3; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.95' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E");
animation: flm-jitter .9s steps(1) infinite; }
.flm-content { box-sizing: border-box; width: 100%; max-width: 72rem; margin: 0 auto; padding: 120px 24px 72px; }
.flm-kicker { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; letter-spacing: .1em; text-transform: uppercase; color: #e8c9a6; }
.flm-title { margin: 16px 0 0; max-width: 46rem; font-size: clamp(36px, 6.2vw, 66px); line-height: 1.04; font-weight: 600; letter-spacing: -.01em; color: #f7efe4; }
.flm-copy { margin: 20px 0 0; max-width: 34rem; font-size: 18px; line-height: 1.65; color: #e3d8cb; }
.flm-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 26px; border-radius: 2px; background: #f2e6d6; color: #1c1511; font-size: 15px; font-weight: 600; text-decoration: none; }
.flm-button:hover { background: #fff8ee; }
.flm-button:focus-visible { outline: 2px solid #f2e6d6; outline-offset: 3px; }
.flm-hero[data-flm-paused] * { animation-play-state: paused !important; }
@keyframes flm-pan { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(12%, 6%) scale(1.12); } }
@keyframes flm-leak { 0%, 100% { opacity: .55; transform: translateY(0); } 38% { opacity: 1; transform: translateY(-4%); } 46% { opacity: .7; } 70% { opacity: .9; transform: translateY(3%); } }
@keyframes flm-jitter { 0% { transform: translate(0, 0); } 20% { transform: translate(-23px, 14px); } 40% { transform: translate(17px, -31px); } 60% { transform: translate(-9px, 27px); } 80% { transform: translate(29px, 5px); } }
@media (min-width: 640px) { .flm-content { padding: 140px 40px 88px; } }
@media (prefers-reduced-motion: reduce) { .flm-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the wash and the grain flicker while the hero is scrolled out of view.
var root = document.querySelector(".flm-hero:not([data-flm-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-flm-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-flm-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The grade, leak and grain freeze into one still frame that looks like a single film scan.
Running cost: Four layers animate transform and opacity only; the grain is a single small tile, so the cost is compositing.
A bleached sky fades into hot sand, and along the horizon the air wobbles: distant mesas bend, and a pale mirage of them floats on the ground below. The wobble is an SVG displacement filter fed by turbulence whose frequency slowly breathes, applied only to the horizon group so the sky and type stay sharp. Suits travel, outdoor gear, summer events and anything about heat.
<section class="hts-hero">
<div class="hts-sky" aria-hidden="true">
<svg class="hts-scene" viewBox="0 0 1440 640" preserveAspectRatio="xMidYMax slice">
<defs>
<linearGradient id="hts-air" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#efe3cc" />
<stop offset=".45" stop-color="#f4dfba" />
<stop offset=".72" stop-color="#f6d6a4" />
</linearGradient>
<linearGradient id="hts-ground" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e9c08c" />
<stop offset=".35" stop-color="#d49a62" />
<stop offset="1" stop-color="#a86a3c" />
</linearGradient>
<radialGradient id="hts-glare" cx=".5" cy=".5" r=".5">
<stop offset="0" stop-color="#fffaf0" stop-opacity=".95" />
<stop offset=".4" stop-color="#fff3dc" stop-opacity=".45" />
<stop offset="1" stop-color="#fff3dc" stop-opacity="0" />
</radialGradient>
<filter id="hts-wobble" x="-2%" y="-20%" width="104%" height="140%">
<feTurbulence type="fractalNoise" baseFrequency=".004 .07" numOctaves="2" seed="4" result="air">
<animate attributeName="baseFrequency" dur="8s" values=".004 .07;.005 .085;.004 .07" repeatCount="indefinite" />
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="air" scale="10" xChannelSelector="R" yChannelSelector="G" />
</filter>
</defs>
<rect width="1440" height="640" fill="url(#hts-air)" />
<circle cx="1060" cy="90" r="320" fill="url(#hts-glare)" />
<g transform="translate(0 92)">
<g filter="url(#hts-wobble)">
<rect x="-20" y="384" width="1480" height="200" fill="url(#hts-ground)" />
<path d="M-20 386 L130 386 L158 356 L262 352 L284 386 L560 386 L586 370 L640 368 L662 386 L960 386 L992 344 L1146 338 L1172 386 L1460 386 L1460 392 L-20 392 Z" fill="#cf9a6c" opacity=".45" />
<path d="M-20 392 L130 392 L158 414 L262 418 L284 392 L560 392 L586 404 L640 406 L662 392 L960 392 L992 424 L1146 430 L1172 392 L1460 392 Z" fill="#f6dcb2" opacity=".5" />
<rect x="-20" y="372" width="1480" height="30" fill="#f8dfb6" opacity=".55" />
</g>
</g>
</svg>
<div class="hts-grain"></div>
</div>
<div class="hts-content">
<p class="hts-kicker">Saltpan Overland</p>
<h1 class="hts-title">Three days across the dry lakes, with camp set before dusk.</h1>
<p class="hts-copy">Guided four-wheel-drive trips for up to eight people. We carry the water, the shade and the spare tyres.</p>
<a class="hts-button" href="#">See the route</a>
</div>
</section>
<style>
/* Change these first:
scale="10" on feDisplacementMap: how far the heat bends the horizon
dur="8s" on the animate tag: shimmer speed
the stop colours in #hts-air and #hts-ground: sky and sand
.hts-grain opacity: grain strength */
.hts-hero { position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: flex-start; min-height: 640px; background: #f1dfc0; font-family: ui-sans-serif, system-ui, sans-serif; }
.hts-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.hts-scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.hts-grain { position: absolute; inset: 0; opacity: .3; mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.hts-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 88px 24px 200px; }
.hts-kicker { margin: 0; font-size: 13px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: #8a4a1e; }
.hts-title { margin: 16px 0 0; max-width: 42rem; font-size: clamp(32px, 5.4vw, 56px); line-height: 1.08; font-weight: 700; letter-spacing: -.015em; color: #2b1a0c; }
.hts-copy { margin: 18px 0 0; max-width: 32rem; font-size: 18px; line-height: 1.6; color: #4d3420; }
.hts-button { display: inline-flex; align-items: center; height: 48px; margin-top: 28px; padding: 0 24px; border-radius: 8px; background: #2b1a0c; color: #fbefdc; font-size: 15px; font-weight: 600; text-decoration: none; }
.hts-button:hover { background: #4a2d16; }
.hts-button:focus-visible { outline: 2px solid #2b1a0c; outline-offset: 3px; }
@media (min-width: 640px) { .hts-content { padding: 96px 40px 210px; } }
</style>
<script>
(function () {
// Stops the shimmer for reduced motion, and pauses it while the hero is off screen.
var root = document.querySelector(".hts-hero:not([data-hts-ready])");
if (!root) return;
root.setAttribute("data-hts-ready", "");
var svg = root.querySelector(".hts-scene");
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (still) { svg.pauseAnimations(); return; }
if (!("IntersectionObserver" in window)) return;
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) svg.unpauseAnimations(); else svg.pauseAnimations();
}).observe(root);
})();
</script>
With reduced motion: The SVG animation is paused at its first frame, leaving a sharp, still horizon with its mirage.
Running cost: One SVG displacement filter re-renders the horizon band each frame, a moderate paint cost kept to that strip and paused off screen.
Drops of indigo, sepia and teal ink open in pale water, spread into feathered clouds and thin out until a new drop lands somewhere else. Each colour absorbs light the way pigment does, so overlapping clouds deepen rather than glow. The blooms gather on the right on wide screens and rise from the bottom on phones, leaving the headline on clean paper. Good for art schools, tea rooms, stationers and calm editorial pages.
<section class="ink-hero">
<canvas class="ink-canvas" aria-hidden="true"></canvas>
<div class="ink-grain" aria-hidden="true"></div>
<div class="ink-content">
<p class="ink-kicker">Sumi & Water Studio</p>
<h1 class="ink-title">Brush lettering for beginners, taught at one long wooden table.</h1>
<p class="ink-copy">Two-hour evening classes with ground ink, real brushes and paper that forgives a shaky first stroke.</p>
<a class="ink-button" href="#">Reserve a seat</a>
</div>
</section>
<style>
/* Grain strength: .ink-grain opacity. Colours and timing live at the top of the script. */
.ink-hero { position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 600px; background: #eee9e0; font-family: ui-sans-serif, system-ui, sans-serif; }
.ink-canvas, .ink-grain { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; pointer-events: none; }
.ink-grain { opacity: .3; mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.ink-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 80px 24px 200px; }
.ink-kicker { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; color: #3a4466; }
.ink-title { margin: 16px 0 0; max-width: 34rem; font-family: ui-serif, Georgia, "Times New Roman", serif; font-size: clamp(34px, 5.4vw, 54px); line-height: 1.1; font-weight: 500; color: #161b2a; }
.ink-copy { margin: 20px 0 0; max-width: 28rem; font-size: 18px; line-height: 1.65; color: #363c4d; }
.ink-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 24px; border-radius: 6px; background: #1c2440; color: #f3efe7; font-size: 15px; font-weight: 600; text-decoration: none; }
.ink-button:hover { background: #2a3458; }
.ink-button:focus-visible { outline: 2px solid #1c2440; outline-offset: 3px; }
@media (min-width: 640px) { .ink-content { padding-left: 40px; padding-right: 40px; } }
@media (min-width: 768px) { .ink-content { padding-top: 96px; padding-bottom: 96px; } }
</style>
<script>
(function () {
// Ink colours as [r, g, b]: indigo, sepia and a deep teal.
var INKS = [[38, 50, 98], [112, 72, 46], [24, 82, 92]];
// The water and paper behind the ink.
var WATER = [238, 233, 224];
// Seconds one bloom takes to open, spread and fade.
var LIFE = 16;
// How dark the ink can get. Keep it modest so the text stays readable.
var STRENGTH = 1.7;
// Where blooms start, as fractions of width and height (kept away from the headline).
var SPOTS = [[0.78, 0.32], [0.62, 0.78], [0.92, 0.64], [0.7, 0.12], [0.86, 0.92], [0.56, 0.45]];
var root = document.querySelector(".ink-hero:not([data-ink-ready])");
if (!root) return;
root.setAttribute("data-ink-ready", "");
var canvas = root.querySelector(".ink-canvas");
var ctx = canvas.getContext("2d");
if (!ctx) return;
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var W = 140, H = 70, aspect = 1.6, narrow = false, img = ctx.createImageData(W, H), raf = 0, onScreen = true, next = 0, blooms = [];
function clock() { return performance.now() / 1000; }
function spawn(born) {
var s = SPOTS[next % SPOTS.length], ink = INKS[next % INKS.length];
next += 1;
// On narrow screens the text fills the width, so ink rises from the bottom edge instead.
var x = narrow ? 0.15 + ((s[0] - 0.5) / 0.45) * 0.7 : s[0];
var y = narrow ? 0.78 + s[1] * 0.25 : s[1];
return { x: x + (Math.random() - 0.5) * 0.06, y: y, born: born, size: 0.16 + Math.random() * 0.08, ink: ink };
}
function seed() {
var now = clock();
next = 0;
blooms = [0, 1, 2, 3, 4].map(function (k) { return spawn(now - LIFE * (0.15 + k * 0.17)); });
}
function draw(t) {
var d = img.data, o = 0, i;
for (i = 0; i < blooms.length; i++) if (t - blooms[i].born > LIFE) blooms[i] = spawn(t);
var live = blooms.map(function (b) {
var a = Math.min(1, Math.max(0, (t - b.born) / LIFE));
var sigma = b.size * (0.3 + 1.1 * Math.sqrt(a));
return { x: b.x * aspect, y: b.y, amp: Math.pow(1 - a, 1.4) * Math.min(1, a * 7), k: 1 / (sigma * sigma), ink: b.ink };
});
for (var y = 0; y < H; y++) {
var ny = y / (H - 1);
for (var x = 0; x < W; x++) {
var nx = (x / (W - 1)) * aspect;
var wx = nx + 0.05 * Math.sin(ny * 9 + t * 0.21) + 0.03 * Math.sin(ny * 27 - t * 0.4) + 0.012 * Math.sin((nx + ny) * 61 + t * 0.5);
var wy = ny + 0.05 * Math.cos(nx * 7 - t * 0.17) + 0.03 * Math.cos(nx * 23 + t * 0.33) + 0.012 * Math.cos((nx - ny) * 57 - t * 0.45);
var ar = 0, ag = 0, ab = 0;
for (i = 0; i < live.length; i++) {
var b = live[i], dx = wx - b.x, dy = wy - b.y;
var q = (dx * dx + dy * dy) * b.k, v = b.amp * Math.exp(-q) * (0.75 + 0.5 * Math.exp(-Math.abs(q - 1.2) * 3));
ar += v * (1 - b.ink[0] / 255); ag += v * (1 - b.ink[1] / 255); ab += v * (1 - b.ink[2] / 255);
}
d[o] = WATER[0] * Math.exp(-ar * STRENGTH);
d[o + 1] = WATER[1] * Math.exp(-ag * STRENGTH);
d[o + 2] = WATER[2] * Math.exp(-ab * STRENGTH);
d[o + 3] = 255;
o += 4;
}
}
ctx.putImageData(img, 0, 0);
}
function loop() {
draw(clock());
if (onScreen && !still) raf = requestAnimationFrame(loop);
}
function resize() {
var box = root.getBoundingClientRect();
aspect = Math.max(0.4, box.width / Math.max(1, box.height));
var wasNarrow = narrow;
narrow = box.width < 700;
H = Math.max(30, Math.min(180, Math.round(W / aspect)));
canvas.width = W; canvas.height = H;
img = ctx.createImageData(W, H);
if (!blooms.length || wasNarrow !== narrow) seed();
draw(clock());
}
new ResizeObserver(resize).observe(root);
new IntersectionObserver(function (entries) {
onScreen = entries[0].isIntersecting;
cancelAnimationFrame(raf);
if (onScreen && !still) raf = requestAnimationFrame(loop);
}).observe(root);
resize();
})();
</script>
With reduced motion: One painted frame of three soft ink clouds stays on the paper with no further movement.
Running cost: A 140 pixel wide canvas recomputes five blooms per frame, around a millisecond, and the loop stops off screen.
Pale lilac, mint, peach and ice blue bands sweep across an off-white surface like light turning on nacre. Two repeating conic gradients rotate around corners outside the frame using an animated angle property, one laid thinly over the other, and soft white lustre spots glide on top. Made for jewellery, skincare, bridal and other quiet luxury pages that want colour you only notice on a second look.
<section class="prl-hero">
<div class="prl-sky" aria-hidden="true">
<div class="prl-nacre prl-n1"></div>
<div class="prl-nacre prl-n2"></div>
<div class="prl-lustre prl-l1"></div>
<div class="prl-lustre prl-l2"></div>
<div class="prl-grain"></div>
</div>
<div class="prl-content">
<p class="prl-kicker">Halden & Pearl · Jewellery repairs</p>
<h1 class="prl-title">Restringing and care for the pearls you plan to keep.</h1>
<p class="prl-copy">Knotted silk, new clasps and a gentle clean, done at the bench in a single afternoon while you wait or post them in.</p>
<a class="prl-button" href="#">Book a bench visit</a>
</div>
</section>
<style>
/* Change these first:
the tints in .prl-n1 (the nacre colours; keep them pale)
--prl-speed: one full turn of the sheen
.prl-grain opacity: grain strength */
@property --prl-a { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
.prl-hero { --prl-speed: 48s;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 600px;
background: #efebe6; font-family: ui-sans-serif, system-ui, sans-serif; }
.prl-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.prl-nacre { position: absolute; inset: 0; --prl-a: 40deg; }
.prl-n1 { background: repeating-conic-gradient(from var(--prl-a) at -10% 115%, #efe9e3 0deg, #d3c3ea 7deg, #f0d2bd 14deg, #c3e2d5 21deg, #ccd8f0 28deg, #efe9e3 36deg);
animation: prl-turn var(--prl-speed) linear -12s infinite; }
.prl-n2 { opacity: .35;
background: repeating-conic-gradient(from var(--prl-a) at 115% -20%, #fbf9f6 0deg, #e2d6f0 12deg, #fbf9f6 22deg, #d8eee4 34deg, #fbf9f6 46deg);
animation: prl-turn calc(var(--prl-speed) * 1.4) linear -30s infinite reverse; }
.prl-lustre { position: absolute; border-radius: 50%; mix-blend-mode: screen; }
.prl-l1 { left: 30%; top: -30%; width: 70%; height: 90%; background: radial-gradient(closest-side, rgba(255, 255, 255, .7), rgba(255, 255, 255, .2) 50%, transparent); animation: prl-glide 26s ease-in-out -6s infinite alternate; }
.prl-l2 { left: -10%; top: 45%; width: 55%; height: 70%; background: radial-gradient(closest-side, rgba(255, 252, 246, .7), transparent); animation: prl-glide 34s ease-in-out -19s infinite alternate-reverse; }
.prl-grain { position: absolute; inset: 0; opacity: .3; mix-blend-mode: soft-light;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.prl-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 96px 24px; }
.prl-kicker { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: #5b5267; }
.prl-title { margin: 18px 0 0; max-width: 40rem; font-family: ui-serif, Georgia, "Times New Roman", serif; font-size: clamp(34px, 5.6vw, 58px); line-height: 1.08; font-weight: 400; color: #24212b; }
.prl-copy { margin: 20px 0 0; max-width: 32rem; font-size: 18px; line-height: 1.65; color: #45404f; }
.prl-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 26px; border-radius: 999px; border: 1px solid #24212b; background: transparent; color: #24212b; font-size: 15px; font-weight: 600; text-decoration: none; }
.prl-button:hover { background: #24212b; color: #f6f2ee; }
.prl-button:focus-visible { outline: 2px solid #24212b; outline-offset: 3px; }
.prl-hero[data-prl-paused] * { animation-play-state: paused !important; }
@keyframes prl-turn { from { --prl-a: 0deg; } to { --prl-a: 360deg; } }
@keyframes prl-glide { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(-18%, 14%) scale(1.2); } }
@media (min-width: 640px) { .prl-content { padding: 110px 40px; } }
@media (prefers-reduced-motion: reduce) { .prl-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the sheen while the hero is scrolled out of view.
var root = document.querySelector(".prl-hero:not([data-prl-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-prl-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-prl-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The nacre bands rest at one angle with the lustre spots in place, a still pearly surface.
Running cost: Two full-size conic gradients repaint as their angle turns, plus two transformed highlights; modest paint cost, no script loop.
Frosted green, blue and moss pools drift over a pale shore colour, all painted by one element that stacks four radial gradients. The centre of each pool is a registered percentage property, so the pools glide to new spots instead of the whole layer sliding. A slow white sheen crosses the surface and a soft-light grain gives it the dull frost of tumbled glass. Fits ceramics, coastal shops, spas and fresh, light brands.
<section class="sgl-hero">
<div class="sgl-sky" aria-hidden="true">
<div class="sgl-glass"></div>
<div class="sgl-sheen"></div>
<div class="sgl-frost"></div>
</div>
<div class="sgl-content">
<p class="sgl-kicker">Low Tide Ceramics</p>
<h1 class="sgl-title">Stoneware glazed in the colours of the shoreline.</h1>
<p class="sgl-copy">Mugs, bowls and serving plates thrown in a seaside workshop and fired in small kilns of thirty pieces at a time.</p>
<a class="sgl-button" href="#">Shop the autumn kiln</a>
</div>
</section>
<style>
/* Change these first:
the four colours in .sgl-glass (frosted greens and blues)
--sgl-speed: how long the pools take to wander
.sgl-frost opacity: how frosted the surface looks */
@property --sgl-x1 { syntax: "<percentage>"; inherits: false; initial-value: 18%; }
@property --sgl-y1 { syntax: "<percentage>"; inherits: false; initial-value: 24%; }
@property --sgl-x2 { syntax: "<percentage>"; inherits: false; initial-value: 82%; }
@property --sgl-y2 { syntax: "<percentage>"; inherits: false; initial-value: 30%; }
@property --sgl-x3 { syntax: "<percentage>"; inherits: false; initial-value: 60%; }
@property --sgl-y3 { syntax: "<percentage>"; inherits: false; initial-value: 88%; }
.sgl-hero { --sgl-speed: 28s;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; justify-content: center; min-height: 580px;
background: #e9efe9; font-family: ui-sans-serif, system-ui, sans-serif; }
.sgl-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.sgl-glass { position: absolute; inset: 0;
background:
radial-gradient(42% 55% at var(--sgl-x1) var(--sgl-y1), rgba(150, 206, 190, .85), transparent 70%),
radial-gradient(40% 50% at var(--sgl-x2) var(--sgl-y2), rgba(146, 186, 210, .75), transparent 70%),
radial-gradient(50% 45% at var(--sgl-x3) var(--sgl-y3), rgba(178, 214, 168, .7), transparent 70%),
radial-gradient(60% 60% at 30% 100%, rgba(120, 178, 176, .55), transparent 70%),
linear-gradient(160deg, #eef3ee, #dfe9e6);
animation: sgl-wander var(--sgl-speed) ease-in-out -7s infinite alternate; }
.sgl-sheen { position: absolute; top: -20%; bottom: -20%; left: 0; width: 45%;
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .42) 46%, rgba(255, 255, 255, .12) 62%, rgba(255, 255, 255, 0) 100%);
animation: sgl-sheen 18s ease-in-out -6s infinite alternate; }
.sgl-frost { position: absolute; inset: 0; opacity: .4; mix-blend-mode: soft-light;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.sgl-content { box-sizing: border-box; width: 100%; max-width: 44rem; margin: 0 auto; padding: 96px 24px; text-align: center; }
.sgl-kicker { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: .2em; text-transform: uppercase; color: #2c5c58; }
.sgl-title { margin: 18px 0 0; font-size: clamp(34px, 5.6vw, 56px); line-height: 1.08; font-weight: 600; letter-spacing: -.015em; color: #0f3332; }
.sgl-copy { margin: 20px auto 0; max-width: 32rem; font-size: 18px; line-height: 1.65; color: #2d4d4b; }
.sgl-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 26px; border-radius: 14px; background: #0f3332; color: #eef6f2; font-size: 15px; font-weight: 600; text-decoration: none; }
.sgl-button:hover { background: #18504e; }
.sgl-button:focus-visible { outline: 2px solid #0f3332; outline-offset: 3px; }
.sgl-hero[data-sgl-paused] * { animation-play-state: paused !important; }
@keyframes sgl-wander {
0% { --sgl-x1: 10%; --sgl-y1: 20%; --sgl-x2: 88%; --sgl-y2: 24%; --sgl-x3: 70%; --sgl-y3: 92%; }
50% { --sgl-x1: 28%; --sgl-y1: 40%; --sgl-x2: 72%; --sgl-y2: 12%; --sgl-x3: 46%; --sgl-y3: 78%; }
100% { --sgl-x1: 20%; --sgl-y1: 70%; --sgl-x2: 80%; --sgl-y2: 58%; --sgl-x3: 36%; --sgl-y3: 18%; }
}
@keyframes sgl-sheen { 0% { transform: translateX(-80%) skewX(-14deg); } 100% { transform: translateX(220%) skewX(-14deg); } }
@media (min-width: 640px) { .sgl-content { padding: 110px 40px; } }
@media (prefers-reduced-motion: reduce) { .sgl-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the wandering pools while the hero is scrolled out of view.
var root = document.querySelector(".sgl-hero:not([data-sgl-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-sgl-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-sgl-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The pools hold their starting positions and the sheen stays put, a calm frosted surface.
Running cost: One stacked gradient repaints as its pool positions animate, plus one transformed sheen; light paint work, no script per frame.
A warm lamp pool lights a dark canvas backdrop, trailing the pointer with a soft lag and casting a faint pool on the floor beneath it. Leave the mouse alone for a couple of seconds and the lamp wanders on its own in a loose figure of eight, so the hero never sits dead. Tabbing to the button swings the light onto it. Suited to photographers, galleries, theatres and quiet product launches.
<section class="spl-hero">
<div class="spl-sky" aria-hidden="true">
<div class="spl-sweep"></div>
<div class="spl-lamp"></div>
<div class="spl-pool"></div>
<div class="spl-grain"></div>
</div>
<div class="spl-content">
<p class="spl-kicker">Studio Ashgrove · Portraits</p>
<h1 class="spl-title">Portraits lit the old way, one lamp at a time.</h1>
<p class="spl-copy">An hour in a quiet studio, a canvas backdrop that has seen forty years of sittings, and prints you choose together on the day.</p>
<a class="spl-button" href="#">Book a sitting</a>
</div>
</section>
<style>
/* Change these first:
--spl-light: lamp colour (r, g, b)
--spl-size: lamp diameter
.spl-sweep: the backdrop colours
EASE and IDLE_AFTER in the script: how closely the lamp follows and when it starts drifting */
.spl-hero { --spl-light: 255, 222, 184; --spl-size: 900px;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 600px;
background: #1b1a19; font-family: ui-sans-serif, system-ui, sans-serif; touch-action: pan-y; }
.spl-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.spl-sweep { position: absolute; inset: 0; background: linear-gradient(180deg, #23211e 0%, #2b2825 58%, #34302c 74%, #1c1a18 100%); }
.spl-lamp { position: absolute; left: 0; top: 0; width: var(--spl-size); height: var(--spl-size); margin: calc(var(--spl-size) / -2) 0 0 calc(var(--spl-size) / -2); border-radius: 50%;
background: radial-gradient(closest-side, rgba(var(--spl-light), .34), rgba(var(--spl-light), .16) 38%, rgba(var(--spl-light), .05) 66%, transparent);
mix-blend-mode: screen; will-change: transform; transform: translate3d(65vw, 260px, 0); }
.spl-pool { position: absolute; left: 0; bottom: -60px; width: 70vw; height: 200px; margin-left: -35vw; border-radius: 50%;
background: radial-gradient(closest-side, rgba(var(--spl-light), .14), transparent); will-change: transform; transform: translate3d(65vw, 0, 0); }
.spl-grain { position: absolute; inset: 0; opacity: .34; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.spl-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 96px 24px; }
.spl-kicker { margin: 0; font-size: 13px; font-weight: 500; letter-spacing: .2em; text-transform: uppercase; color: #d9c6b0; }
.spl-title { margin: 18px 0 0; max-width: 38rem; font-family: ui-serif, Georgia, "Times New Roman", serif; font-size: clamp(34px, 5.6vw, 58px); line-height: 1.08; font-weight: 400; color: #faf4ec; }
.spl-copy { margin: 20px 0 0; max-width: 30rem; font-size: 18px; line-height: 1.65; color: #d8cfc5; }
.spl-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 26px; border-radius: 999px; background: #f4ebe0; color: #1f1b17; font-size: 15px; font-weight: 600; text-decoration: none; }
.spl-button:hover { background: #fff; }
.spl-button:focus-visible { outline: 2px solid #f4ebe0; outline-offset: 3px; }
@media (min-width: 640px) { .spl-content { padding: 110px 40px; } }
@media (prefers-reduced-motion: reduce) { .spl-lamp, .spl-pool { will-change: auto; } }
</style>
<script>
(function () {
// How quickly the lamp catches up with the pointer (0 to 1; smaller is lazier).
var EASE = 0.06;
// Seconds without pointer movement before the lamp starts drifting on its own.
var IDLE_AFTER = 2.5;
var root = document.querySelector(".spl-hero:not([data-spl-ready])");
if (!root) return;
root.setAttribute("data-spl-ready", "");
var lamp = root.querySelector(".spl-lamp");
var pool = root.querySelector(".spl-pool");
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var w = 0, h = 0, x = 0, y = 0, tx = 0, ty = 0, lastInput = -1e9, raf = 0, onScreen = true;
function place() {
lamp.style.transform = "translate3d(" + x.toFixed(1) + "px," + y.toFixed(1) + "px,0)";
pool.style.transform = "translate3d(" + x.toFixed(1) + "px,0,0)";
}
function measure() {
var box = root.getBoundingClientRect();
w = box.width; h = box.height;
if (!x) { x = w * 0.68; y = h * 0.42; }
if (still) { x = w * 0.7; y = h * 0.4; }
place();
}
function frame(now) {
var t = now / 1000;
if (t - lastInput > IDLE_AFTER) {
// Idle: wander slowly in a loose figure of eight.
tx = w * (0.55 + 0.3 * Math.sin(t * 0.23));
ty = h * (0.45 + 0.22 * Math.sin(t * 0.37 + 1.2));
}
x += (tx - x) * EASE;
y += (ty - y) * EASE;
place();
if (onScreen) raf = requestAnimationFrame(frame);
}
function aim(cx, cy) {
var box = root.getBoundingClientRect();
tx = cx - box.left; ty = cy - box.top;
lastInput = performance.now() / 1000;
}
new ResizeObserver(measure).observe(root);
measure();
if (still) return;
root.addEventListener("pointermove", function (e) { aim(e.clientX, e.clientY); }, { passive: true });
// Keyboard users: the lamp swings over whatever link or button has focus.
root.addEventListener("focusin", function (e) {
var r = e.target.getBoundingClientRect();
aim(r.left + r.width / 2, r.top + r.height / 2);
});
new IntersectionObserver(function (entries) {
onScreen = entries[0].isIntersecting;
cancelAnimationFrame(raf);
if (onScreen) raf = requestAnimationFrame(frame);
}).observe(root);
})();
</script>
With reduced motion: The lamp rests to the right of the headline as a fixed warm pool and ignores the pointer.
Running cost: A tiny script eases two transformed layers each frame; painting stays on the compositor and the loop stops off screen.
Low orange, rose and ochre haze drifts along the bottom of a cream sheet while a pale sun sinks and rises behind it. A fibrous paper texture and a multiplied grain sit over the colour, so it reads like ink soaked into cotton stock rather than a screen glow. A good match for print studios, wedding stationers, bakeries and any brand with a handmade, warm voice.
<section class="shp-hero">
<div class="shp-sky" aria-hidden="true">
<div class="shp-haze shp-h1"></div>
<div class="shp-haze shp-h2"></div>
<div class="shp-haze shp-h3"></div>
<div class="shp-haze shp-h4"></div>
<div class="shp-sun"></div>
<div class="shp-paper"></div>
<div class="shp-grain"></div>
</div>
<div class="shp-content">
<p class="shp-kicker">Marigold Press · Letterpress studio</p>
<h1 class="shp-title">Wedding invitations printed by hand on cotton paper.</h1>
<p class="shp-copy">Deep impressions, mixed inks and envelopes lined to match. Proofs arrive by post within ten working days.</p>
<a class="shp-button" href="#">Request a sample pack</a>
</div>
</section>
<style>
/* Change these first:
--shp-paper: the paper colour everything sits on
--shp-orange, --shp-rose, --shp-ochre: the haze colours (r, g, b)
--shp-speed: base drift time; larger is calmer */
.shp-hero { --shp-paper: #f3eadb; --shp-orange: 226, 122, 68; --shp-rose: 204, 104, 104; --shp-ochre: 228, 172, 92; --shp-speed: 30s;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; justify-content: center; min-height: 600px;
background: var(--shp-paper); font-family: ui-sans-serif, system-ui, sans-serif; }
.shp-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.shp-haze { position: absolute; border-radius: 50%; will-change: transform; }
.shp-h1 { left: -20%; bottom: -45%; width: 90%; height: 95%; background: radial-gradient(closest-side, rgba(var(--shp-orange), .8), rgba(var(--shp-orange), .3) 55%, transparent); animation: shp-a var(--shp-speed) ease-in-out -8s infinite alternate; }
.shp-h2 { right: -25%; bottom: -35%; width: 85%; height: 90%; background: radial-gradient(closest-side, rgba(var(--shp-rose), .62), rgba(var(--shp-rose), .2) 55%, transparent); animation: shp-b calc(var(--shp-speed) * 1.3) ease-in-out -14s infinite alternate; }
.shp-h3 { left: 20%; bottom: -20%; width: 70%; height: 55%; background: radial-gradient(closest-side, rgba(var(--shp-ochre), .55), transparent); animation: shp-a calc(var(--shp-speed) * .8) ease-in-out -3s infinite alternate-reverse; }
.shp-h4 { left: 5%; top: -40%; width: 90%; height: 70%; background: radial-gradient(closest-side, rgba(150, 120, 150, .2), transparent); animation: shp-b calc(var(--shp-speed) * 1.6) ease-in-out -20s infinite alternate; }
.shp-sun { position: absolute; left: 50%; bottom: 6%; width: 220px; height: 220px; margin-left: -110px; border-radius: 50%;
background: radial-gradient(closest-side, rgba(255, 236, 206, .95), rgba(250, 200, 150, .5) 45%, transparent);
animation: shp-sink calc(var(--shp-speed) * 1.5) ease-in-out -10s infinite alternate; }
.shp-paper { position: absolute; inset: 0; opacity: .12; mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='320'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.03 .12' numOctaves='4' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .62 0 0 0 0 .55 0 0 0 0 .45 0 0 0 -1.4 1.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23f%29'/%3E%3C/svg%3E"); }
.shp-grain { position: absolute; inset: 0; opacity: .35; mix-blend-mode: multiply;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.shp-content { box-sizing: border-box; width: 100%; max-width: 46rem; margin: 0 auto; padding: 96px 24px 140px; text-align: center; }
.shp-kicker { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: #7a3a1c; }
.shp-title { margin: 18px 0 0; font-family: ui-serif, Georgia, "Times New Roman", serif; font-size: clamp(34px, 5.6vw, 58px); line-height: 1.08; font-weight: 500; color: #2a1a10; }
.shp-copy { margin: 20px auto 0; max-width: 32rem; font-size: 18px; line-height: 1.65; color: #4a3426; }
.shp-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 26px; border-radius: 4px; background: #2a1a10; color: #f8efe2; font-size: 15px; font-weight: 600; text-decoration: none; }
.shp-button:hover { background: #452a1a; }
.shp-button:focus-visible { outline: 2px solid #2a1a10; outline-offset: 3px; }
.shp-hero[data-shp-paused] * { animation-play-state: paused !important; }
@keyframes shp-a { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(14%, -8%) scale(1.15); } }
@keyframes shp-b { 0% { transform: translate(0, 0) scale(1.1); } 100% { transform: translate(-16%, -6%) scale(.95); } }
@keyframes shp-sink { 0% { transform: translateY(-30px); opacity: 1; } 100% { transform: translateY(40px); opacity: .7; } }
@media (min-width: 640px) { .shp-content { padding: 110px 40px 150px; } }
@media (prefers-reduced-motion: reduce) { .shp-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the drifting haze while the hero is scrolled out of view.
var root = document.querySelector(".shp-hero:not([data-shp-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-shp-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-shp-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The haze and sun hold still in a low evening glow across the bottom of the paper.
Running cost: Five soft layers drift by transform only and the textures are static tiles, so the browser composites rather than repaints.
Just two inks, harbour navy and fired clay, share the frame and the line between them keeps wandering. The boundary is a linear gradient whose angle and midpoint are registered properties, so both animate, while two soft pools of each colour drift across the seam. A fine dot screen and grain make it look like a two-colour print. Fits record labels, posters, events and bold editorial brands.
<section class="duo-hero">
<div class="duo-sky" aria-hidden="true">
<div class="duo-field"></div>
<div class="duo-pool duo-p1"></div>
<div class="duo-pool duo-p2"></div>
<div class="duo-screen"></div>
<div class="duo-grain"></div>
</div>
<div class="duo-content">
<p class="duo-kicker">Tidewater Records</p>
<h1 class="duo-title">A small label for slow music. New pressings every second Friday.</h1>
<p class="duo-copy">Ambient, folk and quiet jazz on heavyweight vinyl, mastered for a room rather than a phone speaker.</p>
<a class="duo-button" href="#">Hear this month's record</a>
</div>
</section>
<style>
/* Change these first:
--duo-ink and --duo-clay: the two tones (anything dark enough for white text)
--duo-speed: one sweep of the boundary between them
.duo-screen opacity: strength of the printed dot screen */
@property --duo-a { syntax: "<angle>"; inherits: false; initial-value: 125deg; }
@property --duo-p { syntax: "<percentage>"; inherits: false; initial-value: 45%; }
.duo-hero { --duo-ink: #13283f; --duo-clay: #9a4a2e; --duo-speed: 26s;
position: relative; isolation: isolate; overflow: hidden; display: flex; align-items: center; min-height: 580px;
background: var(--duo-clay); font-family: ui-sans-serif, system-ui, sans-serif; }
.duo-sky { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.duo-field { position: absolute; inset: 0; --duo-a: 125deg; --duo-p: 45%;
background: linear-gradient(var(--duo-a), var(--duo-ink) calc(var(--duo-p) - 22%), var(--duo-clay) calc(var(--duo-p) + 26%));
animation: duo-sweep var(--duo-speed) ease-in-out -9s infinite alternate; }
.duo-pool { position: absolute; border-radius: 50%; }
.duo-p1 { left: 45%; top: -35%; width: 70%; height: 110%; background: radial-gradient(closest-side, var(--duo-ink), transparent); opacity: .75; animation: duo-float calc(var(--duo-speed) * 1.2) ease-in-out -4s infinite alternate; }
.duo-p2 { left: -25%; top: 50%; width: 75%; height: 100%; background: radial-gradient(closest-side, var(--duo-clay), transparent); opacity: .7; animation: duo-float calc(var(--duo-speed) * .9) ease-in-out -15s infinite alternate-reverse; }
.duo-screen { position: absolute; inset: 0; opacity: .14; mix-blend-mode: multiply;
background-image: radial-gradient(circle at 50% 50%, #000 0 1.1px, transparent 1.6px); background-size: 5px 5px; }
.duo-grain { position: absolute; inset: 0; opacity: .35; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url%28%23g%29'/%3E%3C/svg%3E"); }
.duo-content { box-sizing: border-box; width: 100%; max-width: 64rem; margin: 0 auto; padding: 96px 24px; }
.duo-kicker { margin: 0; display: inline-block; padding: 6px 10px; border: 1px solid rgba(255, 240, 225, .5); font-size: 12px; font-weight: 700; letter-spacing: .2em; text-transform: uppercase; color: #fbeee2; }
.duo-title { margin: 22px 0 0; max-width: 46rem; font-size: clamp(32px, 5.4vw, 56px); line-height: 1.08; font-weight: 700; letter-spacing: -.015em; color: #fff8f1; }
.duo-copy { margin: 20px 0 0; max-width: 32rem; font-size: 18px; line-height: 1.65; color: #f3e4d8; }
.duo-button { display: inline-flex; align-items: center; height: 48px; margin-top: 32px; padding: 0 24px; background: #fff8f1; color: #13283f; font-size: 15px; font-weight: 700; text-decoration: none; }
.duo-button:hover { background: #fde9d8; }
.duo-button:focus-visible { outline: 2px solid #fff8f1; outline-offset: 3px; }
.duo-hero[data-duo-paused] * { animation-play-state: paused !important; }
@keyframes duo-sweep { 0% { --duo-a: 100deg; --duo-p: 30%; } 100% { --duo-a: 170deg; --duo-p: 62%; } }
@keyframes duo-float { 0% { transform: translate(0, 0) scale(1); } 100% { transform: translate(-14%, 10%) scale(1.18); } }
@media (min-width: 640px) { .duo-content { padding: 110px 40px; } }
@media (prefers-reduced-motion: reduce) { .duo-hero * { animation: none !important; } }
</style>
<script>
(function () {
// Pauses the drift while the hero is scrolled out of view.
var root = document.querySelector(".duo-hero:not([data-duo-ready])");
if (!root || !("IntersectionObserver" in window)) return;
root.setAttribute("data-duo-ready", "");
new IntersectionObserver(function (entries) {
root.toggleAttribute("data-duo-paused", !entries[0].isIntersecting);
}).observe(root);
})();
</script>
With reduced motion: The two tones meet along one fixed diagonal, with the dot screen and grain still in place.
Running cost: One gradient repaints as its angle and midpoint animate, and two pools move by transform; light, with no script per frame.
Gradients band on 8-bit screens when two close colours stretch across a wide area. A faint noise layer breaks the steps up; it is drawn once as an SVG or a small tile and never animated, so it costs almost nothing after the first paint.
Animating a filter: blur() on a large element is one of the most expensive things CSS can do. These effects blur once and then only translate or rotate the blurred layer. The canvas versions cap the pixel ratio, draw at a fraction of the box size and stop their frame loop whenever the block is off screen or the tab is hidden.
Browsers cannot tween between two gradient images, so either move a larger gradient layer with transform, or register the colour stops with @property so they can be interpolated. The @property route works in current Chrome, Edge, Safari and Firefox; older browsers simply show the starting colours.
That is banding: the screen has too few colour steps between your two colours across that distance. Add a subtle noise overlay, pick colours further apart in lightness, or shorten the distance the blend has to cover.
A gradient where colour is set at several points across the area and blended between them, rather than along one line or around one centre. CSS has no native mesh gradient, so it is approximated with overlapping radial gradients or drawn on a canvas.
It can, because the colour under the text changes over time. Test the lightest and darkest moments of the cycle, or put a soft scrim behind the text so contrast stays above 4.5 to 1 throughout.
More collections: Animated Backgrounds in Pure CSS, Particle Backgrounds in Plain JavaScript, Animated Backgrounds for Websites. All animated backgrounds.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.