Aurora gradient
Three soft, blurred colour blobs drift slowly behind a heading.
It imports two small hooks from @/lib/motion and the colour tokens; the zip includes both.
Demo.tsx
"use client";
import { useRef } from "react";
import { useInView } from "@/lib/motion";
import styles from "./demo.module.css";
export default function Demo() {
const root = useRef<HTMLDivElement>(null);
const inView = useInView(root);
return (
<div ref={root} className={`${styles.root} ${inView ? "" : styles.paused}`}>
<div className={styles.sky} aria-hidden="true">
<span className={`${styles.blob} ${styles.one}`} />
<span className={`${styles.blob} ${styles.two}`} />
<span className={`${styles.blob} ${styles.three}`} />
</div>
<div className={styles.copy}>
<span className={styles.kicker}>For villa hosts</span>
<h2 className={styles.heading}>Sell the nights between your bookings</h2>
<p className={styles.sub}>StayGap finds open nights and offers them to guests already staying.</p>
</div>
</div>
);
}
demo.module.css
.root { container-type: inline-size; position: relative; isolation: isolate; width: 100%; height: 100%; display: grid; place-items: center; padding: 20px; overflow: hidden; background: var(--white); }
.sky { position: absolute; inset: 0; z-index: -1; filter: blur(48px); opacity: .6; }
.blob { position: absolute; width: 60%; aspect-ratio: 1; border-radius: 50%; animation: drift 22s ease-in-out infinite alternate; }
.one { left: -10%; top: -30%; background: var(--accent-soft); }
.two { right: -15%; top: 10%; background: var(--sky); animation-duration: 27s; animation-delay: -9s; }
.three { left: 20%; bottom: -45%; background: var(--sand); animation-duration: 31s; animation-delay: -15s; }
@keyframes drift {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(12%, 10%) scale(1.12); }
100% { transform: translate(-8%, 4%) scale(.94); }
}
.paused .blob { animation-play-state: paused; }
.copy { display: grid; justify-items: center; gap: 10px; max-width: 30em; text-align: center; }
.kicker { font-size: 12px; font-weight: 600; color: var(--accent-text); }
.heading { font-size: clamp(22px, 5cqi, 40px); line-height: 1.1; letter-spacing: -0.03em; font-weight: 650; text-wrap: balance; }
.sub { font-size: 14px; color: var(--muted); max-width: 26em; }
@media (prefers-reduced-motion: reduce) {
.blob { animation: none; }
}
Use it for
- A single hero or closing section that needs warmth
- Brand moments where the palette should feel soft
Avoid
- A glowing gradient behind hero text is a common generated UI tell, so keep it faint and slow
- Behind long text or dense UI, where contrast suffers
- Every section of a page
How it works
Three absolutely positioned circles in theme colours sit in a layer with a single blur filter. Each runs the same transform keyframes (translate and scale) with a different duration and negative delay, alternating, so they never line up. Animation pauses through animation-play-state when the section is off screen.
Accessibility
The layer is aria-hidden and the heading keeps full contrast on a light base. Reduced motion stops the drift and keeps the soft gradient.