Backgrounds
Waves
An SVG wave divider and layered backdrop for section transitions and hero bottoms — pure CSS, no canvas.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/waves.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Waves" component — an SVG wave
divider/backdrop for section transitions and hero bottoms. Its only
dependency is a cn() class merger (clsx + tailwind-merge). No hooks, no
events, no browser APIs, so it must stay a server component — do NOT add
"use client".
Contract
- export function Waves(props): props extend React.ComponentProps<"div">
(rest props spread onto the root div) plus:
- variant?: "divider" | "backdrop" (default "divider") — "divider" renders
a standalone wave band with no content slot, meant to sit between two
sections; "backdrop" fills the container with layered waves anchored to
one edge and renders children above them.
- layers?: 1 | 2 | 3 (default 3) — how many wave layers to stack, back to
front.
- animated?: boolean (default true) — horizontal drift; false renders a
static wave.
- flip?: boolean (default false) — mirrors the wave(s) vertically, for use
at the top of the following section so two dividers sandwich a panel.
- children only apply to variant="backdrop"; the "divider" variant is pure
decoration and ignores them.
Behavior
- Three hand-drawn layer recipes (back/mid/front), each a cubic-bezier path
in a `viewBox="0 0 200 100"` `<svg preserveAspectRatio="none">`. Every path
has a 100-unit period repeated twice across the 200-unit viewBox, so the
<svg> can be rendered at width: 200% and translated by exactly -50% to loop
seamlessly — the second period slides in identical to the first, so the
drift never visibly resets or seams.
- `layers` slices the back-to-front recipe array from the end, so layers=1
shows just the front (boldest, shortest, fastest) layer, layers=2 adds the
mid layer, layers=3 shows all three. Crest position and frequency differ
per layer (front is higher-frequency and shorter-amplitude than back) so
the stack reads as depth rather than a single wave repeated three times.
- animated=true: each <svg> gets one shared @keyframes loop
(translateX(0) -> translateX(-50%)) via a Tailwind arbitrary-value class,
with a per-layer duration passed in as a CSS custom property
(--zy-waves-duration) set through inline style — back is slowest (24s),
front is fastest (12s), so the layers visibly drift at different speeds
instead of moving in lockstep.
- The @keyframes ship inside the component via a React 19 hoisted
<style href="zyeon-waves" precedence="medium"> tag — no Tailwind config
edits, and multiple instances dedupe to one style tag by href.
- prefers-reduced-motion: the animation class carries a
motion-reduce:[animation:none] pair at the same specificity as the
animated class (not an inline style override, which reduced-motion could
not out-rank) — the wave stays visible and static, no functional loss.
- variant="divider": the root div itself is h-16 sm:h-24 w-full
overflow-hidden and aria-hidden="true" (the whole element is decoration,
there is no content slot). flip applies -scale-y-100 to that same root.
- variant="backdrop": the root is relative isolate overflow-hidden; an
aria-hidden, pointer-events-none absolute layer (inset-x-0 bottom-0, h-2/3
sm:h-3/4) holds the wave <svg>s, with flip repositioning that layer to
top-0 and mirroring it (-scale-y-100) instead of moving children. Children
render in a separate relative z-10 wrapper above the wave layer, so the
waves never intercept clicks or sit over text.
Rendering & styling
- Semantic tokens only. Each layer's fill is one of fill-primary/10,
fill-primary/20, fill-primary/30 — back to front, alpha rising with each
layer so the closest wave reads as most solid. No hex / rgb() / oklch()
anywhere, so the waves re-skin themselves with the host theme and dark
mode automatically.
- Merge consumer className via cn() on the root div — for "divider" this
sets the band's own background (match the section above it so the flat
area above the wave crest reads as a seamless continuation); for
"backdrop" it sets the container's background/rounding/border.
Customization levers
- Layer count and speed: layers picks how many of the three recipes render;
each recipe's duration field is the single knob for that layer's drift
speed, independent of the others.
- Shape: each layer's `d` is a self-contained hand-drawn path with a
100-unit period — swap amplitude (the control-point y offsets) or
frequency (add more control-point pairs per 100 units) without touching
the animation or layering logic, as long as the shift by -50% still lands
on an identical period.
- Palette: swap fill-primary/* for var(--chart-1..5) or --secondary/--accent
tokens to re-tint the waves independent of the primary color.
- Sandwich usage: pair two "divider" instances — flip on the one that opens
a panel, plain on the one that closes it — to bookend a section with a
consistent wave silhouette top and bottom.
- Static usage: set animated={false} anywhere a still illustration is
preferred over motion (e.g. print/export views, or alongside other moving
decoration to avoid competing animations).Concepts
- Seamless double-width loop — every layer's path repeats every 100 viewBox units; the
<svg>is drawn at 200% width (two periods) and translated by exactly -50%, so the frame at the end of the loop is pixel-identical to the frame at the start — no jump, no visible seam, regardless of loop duration. - Layered parallax silhouette — three recipes (back/mid/front) differ in crest position, frequency and alpha, and each drifts at its own duration; stacking them reads as depth rather than a single wave copy-pasted three times.
- Divider vs. backdrop —
variant="divider"is a standalone, fully decorative band with no content slot (aria-hiddenon the root itself);variant="backdrop"anchors the same wave layers to one edge of a container and renderschildrenin a separatez-10layer above them. - Vertical flip for sandwiching —
flipmirrors the wave vertically instead of re-drawing it, so the same component can open a panel (flipped) and close it (plain) with a matching silhouette top and bottom. - Reduced-motion honesty — the drift animation and its
motion-reduce:[animation:none]override live in the same Tailwind arbitrary-value class (not a competing inline style), so reduced-motion reliably freezes the wave in place instead of losing to specificity — the shape stays visible, only the motion stops. - Server-component purity — no hooks, no events, no
"use client": the wave renders on the server and animates entirely in CSS.