Topographic lines that slowly shift like terrain, with a vignette for your content.
Find your bearings
Customize
Usage
import { ContourLines } from "@/components/motionkit/contour-lines";
<ContourLines className="h-96 rounded-3xl">
<h1>Your hero</h1>
</ContourLines>Motion
The exact curves this component runs, plotted with Motion's own maths.
Terrain shift
Runs on its own
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/contour-lines.tsx.
"use client";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type ContourLinesProps = {
children?: ReactNode;
/** Number of contour lines. */
lines?: number;
/** How tall the hills are, 0–1. */
amplitude?: number;
/** Seconds for one full shift of the terrain. */
duration?: number;
color?: string;
className?: string;
};
const W = 400;
const H = 240;
const POINTS = 48;
/** One contour as an SVG path; the same point count at every phase so Motion can morph between them. */
function contour(index: number, total: number, amplitude: number, phase: number) {
const baseline = ((index + 1) / (total + 1)) * H;
const height = amplitude * 38;
let d = "";
for (let i = 0; i <= POINTS; i++) {
const x = (i / POINTS) * W;
const t = x / W;
const y =
baseline +
Math.sin(t * Math.PI * 2 + phase + index * 0.55) * height * 0.6 +
Math.sin(t * Math.PI * 5 - phase * 1.3 + index * 0.9) * height * 0.25;
d += `${i ? "L" : "M"}${x.toFixed(1)} ${y.toFixed(1)}`;
}
return d;
}
export function ContourLines({
children,
lines = 14,
amplitude = 0.6,
duration = 16,
color = "#7D9E9D",
className = "",
}: ContourLinesProps) {
const reduceMotion = useReducedMotion();
return (
<div className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}>
<svg aria-hidden viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" className="absolute inset-0 -z-10 h-full w-full">
{Array.from({ length: lines }, (_, index) => {
const frames = [0, Math.PI * 0.66, Math.PI * 1.33, Math.PI * 2].map((phase) => contour(index, lines, amplitude, phase));
return (
<motion.path
key={index}
d={frames[0]}
fill="none"
stroke={color}
strokeOpacity={0.12 + (index % 4 === 0 ? 0.16 : 0)}
strokeWidth={index % 4 === 0 ? 1.4 : 1}
vectorEffect="non-scaling-stroke"
animate={reduceMotion ? undefined : { d: frames }}
transition={{ duration, repeat: Infinity, ease: "linear" }}
/>
);
})}
</svg>
{/* vignette keeps text readable in the middle */}
<div aria-hidden className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_center,#0C1010cc,transparent_75%)]" />
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| lines | number | 14 | Number of contour lines. |
| amplitude | number | 0.6 | How tall the hills are, 0 to 1. |
| duration | number | 16 | Seconds for one full shift. |
| color | string | "#7D9E9D" | Line colour. |
Ambient by default
Aurora Background
Drifting, blurred color fields for hero sections and empty states.
Move your cursor
Dot Glow Background
A quiet dot grid that lights up in teal around the pointer.
Warm, textured, alive
Grain Gradient
Slow, drifting colour with a fine film grain, for warm editorial heroes.