About
Contur line effect 21 st dev
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds 21 st Contour Line to your project and fits it to your theme.
Code
How 21 st Contour Line is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
import React, { useEffect, useRef, useState } from "react";
type Pt = [number, number];
interface Shockwave {
x: number;
y: number;
radius: number;
intensity: number;
}
interface RippleRing {
id: number;
x: number;
y: number;
size: number;
color: string;
}
const css = `
* {
box-sizing: border-box;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #ffffff;
color: #000000;
}
@media (prefers-color-scheme: dark) {
html, body {
background: #050505;
color: #ffffff;
}
}
.contour-hero {
--bg: #ffffff;
--fg: #000000;
--border-inset: 24px;
--border-ring: 18px;
--glass-glow: rgba(255, 255, 255, 0.7);
--glass-base: rgba(255, 255, 255, 0.2);
--glass-shade: rgba(0, 0, 0, 0.2);
--fx: -999px;
--fy: -999px;
--push: 0px;
position: fixed;
inset: 0;
background: var(--bg);
color: var(--fg);
cursor: crosshair;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
.contour-hero {
--bg: #050505;
--fg: #ffffff;
--glass-glow: rgba(255, 255, 255, 0.45);
--glass-base: rgba(0, 0, 0, 0.35);
--glass-shade: rgba(255, 255, 255, 0.1);
color-scheme: dark;
}
}
.contour-hero canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
/* Outer decorative liquid glass frame */
.contour-hero .glass-frame {
position: absolute;
z-index: 10;
inset: calc(var(--border-inset) - var(--push));
border-radius: 36px;
padding: var(--border-ring);
pointer-events: none;
background:
radial-gradient(280px 280px at calc(var(--fx) - var(--border-inset)) calc(var(--fy) - var(--border-inset)), var(--glass-glow), transparent 75%),
linear-gradient(135deg, var(--glass-glow) 0%, transparent 35%, transparent 65%, var(--glass-shade) 100%),
var(--glass-base);
-webkit-backdrop-filter: blur(10px) saturate(1.8);
backdrop-filter: blur(10px) saturate(1.8);
backdrop-filter: url(#contour-refract) blur(6px) saturate(1.8);
box-shadow:
inset 0 1px 2px var(--glass-glow),
inset 0 -1px 2px var(--glass-shade),
inset 0 0 0 1px color-mix(in srgb, var(--fg) 24%, transparent);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
transition: inset 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.contour-hero .glass-inner-rim {
position: absolute;
z-index: 11;
pointer-events: none;
inset: calc(var(--border-inset) + var(--border-ring) - var(--push));
border-radius: calc(36px - var(--border-ring));
box-shadow:
0 0 0 1.5px color-mix(in srgb, var(--fg) 32%, transparent),
0 1px 3px 1px var(--glass-glow);
transition: inset 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* Outward ripple ring element */
.contour-hero .ripple-ring {
position: absolute;
border-radius: 50%;
pointer-events: none;
z-index: 20;
transform: translate(-50%, -50%) scale(0.1);
border: 2px solid currentColor;
box-shadow: 0 0 18px currentColor, inset 0 0 12px currentColor;
animation: rippleExpand 0.85s cubic-bezier(0.1, 0.7, 0.2, 1) forwards;
}
@keyframes rippleExpand {
0% {
transform: translate(-50%, -50%) scale(0.1);
opacity: 0.95;
}
60% {
opacity: 0.5;
}
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0;
border-width: 1px;
}
}
`;
const CELL = 5;
const L0 = 0.08;
const STEP = 0.022;
const N = 42;
const STYLES = [
{ w: 0.5, a: 0.55 },
{ w: 1.0, a: 0.8 },
{ w: 1.8, a: 0.95 },
{ w: 3.2, a: 1.0 },
];
const levelClass = (n: number) =>
n % 8 === 0 ? 3 : n % 4 === 0 ? 2 : n % 2 === 0 ? 1 : 0;
export default function App() {
const heroRef = useRef<HTMLElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const shockwavesRef = useRef<Shockwave[]>([]);
const [ripples, setRipples] = useState<RippleRing[]>([]);
const rippleIdRef = useRef(0);
useEffect(() => {
const canvas = canvasRef.current;
const hero = heroRef.current;
if (!canvas || !hero) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let W = 0,
H = 0,
nx = 0,
ny = 0,
dpr = 1;
let fieldData = new Float32Array(0);
let mx = 0.5,
my = 0.5,
tx = 0.5,
ty = 0.5;
let amp = 0,
intro = 0,
tt = 0,
last = 0,
prev: number | null = null;
let borderInset = 24;
let borderRing = 18;
let rafId = 0;
// Offscreen raster & SDF distance map for the 21st.dev vector logo
const logoCanvas = document.createElement("canvas");
const logoCtx = logoCanvas.getContext("2d");
let logoDistMap: Float32Array | null = null;
let mapW = 0,
mapH = 0;
const getInkColor = () =>
window.matchMedia("(prefers-color-scheme: dark)").matches
? "#ffffff"
: "#000000";
// Generate exact raster and Euclidean distance field for 21st.dev logo
// Directly mapped from the 512x512 pixel coordinates of the reference image
const updateLogoDistanceMap = () => {
if (!logoCtx) return;
mapW = nx + 1;
mapH = ny + 1;
logoCanvas.width = mapW;
logoCanvas.height = mapH;
logoCtx.clearRect(0, 0, mapW, mapH);
logoCtx.fillStyle = "#ffffff";
const logoPixelSize = Math.min(W, H) * 0.64;
const scale = logoPixelSize / 512 / CELL;
const offsetX = (mapW - 512 * scale) * 0.5;
const offsetY = (mapH - 512 * scale) * 0.5;
logoCtx.save();
logoCtx.translate(offsetX, offsetY);
logoCtx.scale(scale, scale);
logoCtx.beginPath();
// --- SHAPE 1: Top-Right block ("1") ---
logoCtx.moveTo(225, 74);
logoCtx.lineTo(438 - 56, 74);
logoCtx.arcTo(438, 74, 438, 74 + 56, 56);
logoCtx.lineTo(438, 332);
logoCtx.lineTo(316, 332);
logoCtx.lineTo(316, 186);
logoCtx.arcTo(316, 180, 310, 180, 6);
logoCtx.lineTo(225, 180);
logoCtx.closePath();
// --- SHAPE 2: Bottom-Left block ("2") ---
logoCtx.moveTo(74, 180);
logoCtx.lineTo(225, 180);
logoCtx.lineTo(225, 326);
logoCtx.arcTo(225, 332, 231, 332, 6);
logoCtx.lineTo(316, 332);
logoCtx.lineTo(316, 438);
logoCtx.lineTo(74 + 56, 438);
logoCtx.arcTo(74, 438, 74, 438 - 56, 56);
logoCtx.lineTo(74, 349);
logoCtx.lineTo(212, 273);
logoCtx.quadraticCurveTo(218, 268, 210, 268);
logoCtx.lineTo(74, 268);
logoCtx.closePath();
logoCtx.fill();
logoCtx.restore();
// Build exact Euclidean Signed Distance Transform (SDF) from raster image
const imgData = logoCtx.getImageData(0, 0, mapW, mapH).data;
logoDistMap = new Float32Array(mapW * mapH);
const INF = 1e6;
const insideMap = new Uint8Array(mapW * mapH);
for (let idx = 0; idx < mapW * mapH; idx++) {
insideMap[idx] = imgData[idx * 4 + 3] > 127 ? 1 : 0;
}
const R = 24;
for (let y = 0; y < mapH; y++) {
for (let x = 0; x < mapW; x++) {
const idx = y * mapW + x;
const inside = insideMap[idx];
let minD2 = INF;
const yMin = Math.max(0, y - R),
yMax = Math.min(mapH - 1, y + R);
const xMin = Math.max(0, x - R),
xMax = Math.min(mapW - 1, x + R);
for (let qy = yMin; qy <= yMax; qy++) {
const row = qy * mapW;
const dy2 = (qy - y) * (qy - y);
if (dy2 >= minD2) continue;
for (let qx = xMin; qx <= xMax; qx++) {
if (insideMap[row + qx] !== inside) {
const d2 = (qx - x) * (qx - x) + dy2;
if (d2 < minD2) minD2 = d2;
}
}
}
const dist = Math.sqrt(minD2);
logoDistMap[idx] = inside ? -dist : dist;
}
}
};
const resize = () => {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = canvas.clientWidth;
H = canvas.clientHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
const comp = getComputedStyle(hero);
borderInset = parseFloat(comp.getPropertyValue("--border-inset")) || 24;
borderRing = parseFloat(comp.getPropertyValue("--border-ring")) || 18;
nx = Math.ceil(W / CELL) + 1;
ny = Math.ceil(H / CELL) + 1;
fieldData = new Float32Array((nx + 1) * (ny + 1));
updateLogoDistanceMap();
};
const evaluateField = (t: number) => {
const sx = 6 / Math.max(W, 600);
let k = 0;
const shockwaves = shockwavesRef.current;
for (let j = 0; j <= ny; j++) {
const py = j * CELL;
for (let i = 0; i <= nx; i++, k++) {
const px = i * CELL;
const x = px * sx;
const y = py * sx;
// Gentle background topographic flow
let v =
Math.sin(x * 1.15 + t * 0.08 + 1.3 * Math.sin(y * 0.8)) * 0.11 +
Math.sin(y * 0.95 - x * 0.5 + t * 0.06) * 0.09 +
Math.sin((x + y) * 1.7 + t * 0.04) * 0.04;
// Subtle cursor elevation
const dxm = (px - mx * W) / (W * 0.11);
const dym = (py - my * H) / (H * 0.11);
v += 0.22 * Math.exp(-(dxm * dxm + dym * dym));
// Crisp 21st.dev logo contour elevation via exact SDF distance map
if (logoDistMap) {
const d = logoDistMap[j * mapW + i];
if (d < 14) {
const edgeSlope = 1 / (1 + Math.exp(d * 0.95));
const innerTerrace = d < 0 ? Math.min(0.18, -d * 0.018) : 0;
v += edgeSlope * 0.78 + innerTerrace;
}
}
// Ripple shockwaves from border interactions
for (let s = 0; s < shockwaves.length; s++) {
const sw = shockwaves[s];
const d = Math.hypot(px - sw.x, py - sw.y);
const ringDist = d - sw.radius;
if (Math.abs(ringDist) < 70) {
const wave = Math.cos((ringDist / 70) * Math.PI) * 0.5 + 0.5;
v += Math.sin(ringDist * 0.18) * wave * sw.intensity;
}
}
fieldData[k] = 0.5 + amp * v;
}
}
};
const drawContourLines = () => {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
// Clip strictly within inner glass border
ctx.save();
ctx.beginPath();
const clipX = borderInset + borderRing;
const clipY = borderInset + borderRing;
const clipW = W - (borderInset + borderRing) * 2;
const clipH = H - (borderInset + borderRing) * 2;
const radius = Math.max(0, 36 - borderRing);
if (typeof ctx.roundRect === "function") {
ctx.roundRect(
clipX,
clipY,
Math.max(0, clipW),
Math.max(0, clipH),
radius
);
} else {
ctx.rect(clipX, clipY, Math.max(0, clipW), Math.max(0, clipH));
}
ctx.clip();
ctx.strokeStyle = getInkColor();
ctx.lineJoin = "round";
const w = nx + 1;
for (let pass = 0; pass < STYLES.length; pass++) {
ctx.beginPath();
for (let n = 0; n < N; n++) {
if (levelClass(n) !== pass) continue;
const L = L0 + n * STEP;
for (let j = 0; j < ny; j++) {
for (let i = 0; i < nx; i++) {
const p = j * w + i;
const a = fieldData[p];
const b = fieldData[p + 1];
const c = fieldData[p + w + 1];
const d = fieldData[p + w];
if (L < Math.min(a, b, c, d) || L > Math.max(a, b, c, d))
continue;
const cs =
((a > L ? 1 : 0) << 3) |
((b > L ? 1 : 0) << 2) |
((c > L ? 1 : 0) << 1) |
(d > L ? 1 : 0);
if (cs === 0 || cs === 15) continue;
const x0 = i * CELL;
const y0 = j * CELL;
const T: Pt = [x0 + (CELL * (L - a)) / (b - a), y0];
const R: Pt = [x0 + CELL, y0 + (CELL * (L - b)) / (c - b)];
const B: Pt = [x0 + (CELL * (L - d)) / (c - d), y0 + CELL];
const E: Pt = [x0, y0 + (CELL * (L - a)) / (d - a)];
let s: Pt[] = [];
switch (cs) {
case 1:
case 14:
s = [E, B];
break;
case 2:
case 13:
s = [B, R];
break;
case 3:
case 12:
s = [E, R];
break;
case 4:
case 11:
s = [T, R];
break;
case 5:
s = [T, E, B, R];
break;
case 6:
case 9:
s = [T, B];
break;
case 7:
case 8:
s = [T, E];
break;
case 10:
s = [T, R, E, B];
break;
}
for (let q = 0; q < s.length; q += 2) {
ctx.moveTo(s[q][0], s[q][1]);
ctx.lineTo(s[q + 1][0], s[q + 1][1]);
}
}
}
}
ctx.lineWidth = STYLES[pass].w;
ctx.globalAlpha = STYLES[pass].a;
ctx.stroke();
}
ctx.restore();
ctx.globalAlpha = 1;
};
const loop = (ts: number) => {
const dt = prev === null ? 0 : (ts - prev) / 1000;
prev = ts;
intro += dt;
tt += dt;
amp = Math.min(1, intro / 2.4);
amp = 1 - Math.pow(1 - amp, 3);
mx += (tx - mx) * 0.08;
my += (ty - my) * 0.08;
const shockwaves = shockwavesRef.current;
for (let i = shockwaves.length - 1; i >= 0; i--) {
const sw = shockwaves[i];
sw.radius += 240 * dt;
sw.intensity *= Math.pow(0.1, dt);
if (sw.radius > Math.max(W, H) * 0.8 || sw.intensity < 0.01) {
shockwaves.splice(i, 1);
}
}
if (ts - last > 30 || amp < 1) {
last = ts;
evaluateField(tt);
drawContourLines();
}
rafId = requestAnimationFrame(loop);
};
const handleMove = (e: globalThis.PointerEvent) => {
hero.style.setProperty("--fx", `${e.clientX}px`);
hero.style.setProperty("--fy", `${e.clientY}px`);
tx = e.clientX / window.innerWidth;
ty = e.clientY / window.innerHeight;
};
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleSchemeChange = () => {
drawContourLines();
};
resize();
window.addEventListener("pointermove", handleMove);
window.addEventListener("resize", resize);
mediaQuery.addEventListener("change", handleSchemeChange);
rafId = requestAnimationFrame(loop);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("pointermove", handleMove);
window.removeEventListener("resize", resize);
mediaQuery.removeEventListener("change", handleSchemeChange);
};
}, []);
const handlePointerDown = (e: React.PointerEvent<HTMLElement>) => {
const hero = heroRef.current;
if (!hero) return;
const comp = getComputedStyle(hero);
const borderInset = parseFloat(comp.getPropertyValue("--border-inset")) || 24;
const borderRing = parseFloat(comp.getPropertyValue("--border-ring")) || 18;
const edgeThreshold = borderInset + borderRing + 25;
const W = hero.clientWidth;
const H = hero.clientHeight;
const x = e.clientX;
const y = e.clientY;
const isNearEdge =
x <= edgeThreshold ||
x >= W - edgeThreshold ||
y <= edgeThreshold ||
y >= H - edgeThreshold;
if (isNearEdge) {
// Push glass border outward
hero.style.setProperty("--push", "10px");
setTimeout(() => {
hero.style.setProperty("--push", "0px");
}, 180);
// Internal heightfield shockwave
shockwavesRef.current.push({
x,
y,
radius: 5,
intensity: 0.35,
});
const inkColor = window.matchMedia("(prefers-color-scheme: dark)").matches
? "#ffffff"
: "#000000";
const size = Math.max(W, H) * 0.45;
// Spawn 3 concentric outward ripple rings
for (let i = 0; i < 3; i++) {
setTimeout(() => {
const id = ++rippleIdRef.current;
setRipples((prev) => [
...prev,
{ id, x, y, size, color: inkColor },
]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== id));
}, 900);
}, i * 140);
}
}
};
return (
<header
className="contour-hero"
ref={heroRef}
onPointerDown={handlePointerDown}
>
<style>{css}</style>
{/* SVG filter for liquid glass border refraction */}
<svg
width="0"
height="0"
style={{ position: "absolute" }}
aria-hidden="true"
focusable="false"
>
<filter
id="contour-refract"
x="0"
y="0"
width="100%"
height="100%"
colorInterpolationFilters="sRGB"
>
<feTurbulence
type="fractalNoise"
baseFrequency="0.012 0.03"
numOctaves={2}
seed={4}
result="noise"
/>
<feGaussianBlur in="noise" stdDeviation="2" result="soft" />
<feDisplacementMap
in="SourceGraphic"
in2="soft"
scale="26"
xChannelSelector="R"
yChannelSelector="G"
/>
</filter>
</svg>
<canvas
ref={canvasRef}
role="img"
aria-label="Topographic 21st.dev logo contour map"
/>
<div className="glass-frame" aria-hidden="true" />
<div className="glass-inner-rim" aria-hidden="true" />
{ripples.map((r) => (
<div
key={r.id}
className="ripple-ring"
style={{
width: `${r.size}px`,
height: `${r.size}px`,
left: `${r.x}px`,
top: `${r.y}px`,
color: r.color,
}}
/>
))}
</header>
);
}





