Background
Waves
Interactive wave animations with cursor-following physics and Perlin noise generation. Perfect for React applications requiring fluid background effects with Next.js integration and TypeScript support.
Powered by
Loading component...
import { Waves } from '@/components/ui/shadcn-io/waves';const Example = () => ( <div className="relative w-full h-[400px] bg-white rounded-lg overflow-hidden border"> <div className="absolute inset-0"> <Waves lineColor="rgba(0, 0, 0, 0.3)" backgroundColor="white" waveSpeedX={0.02} waveSpeedY={0.01} waveAmpX={40} waveAmpY={20} friction={0.9} tension={0.01} maxCursorMove={120} xGap={12} yGap={36} /> </div> <div className="relative z-10 p-8"> <h3 className="text-2xl font-bold mb-2 text-black">Interactive Waves</h3> <p className="text-gray-600">Move your mouse to interact with the waves</p> </div> </div>);export default Example;
'use client';import { useRef, useEffect } from "react"import { cn } from "@/lib/utils"interface WavesProps { /** * Color of the wave lines */ lineColor?: string /** * Background color of the container */ backgroundColor?: string waveSpeedX?: number waveSpeedY?: number waveAmpX?: number waveAmpY?: number xGap?: number yGap?: number friction?: number tension?: number maxCursorMove?: number className?: string}class Grad { x: number y: number z: number constructor(x: number, y: number, z: number) { this.x = x this.y = y this.z = z } dot2(x: number, y: number) { return this.x * x + this.y * y }}class Noise { grad3: Grad[] p: number[] perm: number[] gradP: Grad[] constructor(seed = 0) { this.grad3 = [ new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0), new Grad(-1, -1, 0), new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1), new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1), ] this.p = [ 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, ] this.perm = new Array(512) this.gradP = new Array(512) this.seed(seed) } seed(seed: number) { if (seed > 0 && seed < 1) seed *= 65536 seed = Math.floor(seed) if (seed < 256) seed |= seed << 8 for (let i = 0; i < 256; i++) { let v = i & 1 ? this.p[i] ^ (seed & 255) : this.p[i] ^ ((seed >> 8) & 255) this.perm[i] = this.perm[i + 256] = v this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12] } } fade(t: number) { return t * t * t * (t * (t * 6 - 15) + 10) } lerp(a: number, b: number, t: number) { return (1 - t) * a + t * b } perlin2(x: number, y: number) { let X = Math.floor(x), Y = Math.floor(y) x -= X y -= Y X &= 255 Y &= 255 const n00 = this.gradP[X + this.perm[Y]].dot2(x, y) const n01 = this.gradP[X + this.perm[Y + 1]].dot2(x, y - 1) const n10 = this.gradP[X + 1 + this.perm[Y]].dot2(x - 1, y) const n11 = this.gradP[X + 1 + this.perm[Y + 1]].dot2(x - 1, y - 1) const u = this.fade(x) return this.lerp( this.lerp(n00, n10, u), this.lerp(n01, n11, u), this.fade(y), ) }}export function Waves({ lineColor = "hsl(var(--foreground))", backgroundColor = "white", waveSpeedX = 0.0125, waveSpeedY = 0.005, waveAmpX = 32, waveAmpY = 16, xGap = 10, yGap = 32, friction = 0.925, tension = 0.005, maxCursorMove = 100, className,}: WavesProps) { const containerRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null) const ctxRef = useRef<CanvasRenderingContext2D | null>(null) const boundingRef = useRef({ width: 0, height: 0, left: 0, top: 0 }) const noiseRef = useRef(new Noise(Math.random())) const linesRef = useRef<Array<Array<{ x: number y: number wave: { x: number; y: number } cursor: { x: number; y: number; vx: number; vy: number } }>>>([]) const mouseRef = useRef({ x: -10, y: 0, lx: 0, ly: 0, sx: 0, sy: 0, v: 0, vs: 0, a: 0, set: false, }) useEffect(() => { const canvas = canvasRef.current const container = containerRef.current if (!canvas || !container) return ctxRef.current = canvas.getContext("2d") function setSize() { if (!container) return boundingRef.current = container.getBoundingClientRect() if (canvas) { canvas.width = boundingRef.current.width canvas.height = boundingRef.current.height } } function setLines() { const { width, height } = boundingRef.current linesRef.current = [] const oWidth = width + 200, oHeight = height + 30 const totalLines = Math.ceil(oWidth / xGap) const totalPoints = Math.ceil(oHeight / yGap) const xStart = (width - xGap * totalLines) / 2 const yStart = (height - yGap * totalPoints) / 2 for (let i = 0; i <= totalLines; i++) { const pts = [] for (let j = 0; j <= totalPoints; j++) { pts.push({ x: xStart + xGap * i, y: yStart + yGap * j, wave: { x: 0, y: 0 }, cursor: { x: 0, y: 0, vx: 0, vy: 0 }, }) } linesRef.current.push(pts) } } function movePoints(time: number) { const lines = linesRef.current const mouse = mouseRef.current const noise = noiseRef.current lines.forEach((pts) => { pts.forEach((p) => { const move = noise.perlin2( (p.x + time * waveSpeedX) * 0.002, (p.y + time * waveSpeedY) * 0.0015, ) * 12 p.wave.x = Math.cos(move) * waveAmpX p.wave.y = Math.sin(move) * waveAmpY const dx = p.x - mouse.sx, dy = p.y - mouse.sy const dist = Math.hypot(dx, dy), l = Math.max(175, mouse.vs) if (dist < l) { const s = 1 - dist / l const f = Math.cos(dist * 0.001) * s p.cursor.vx += Math.cos(mouse.a) * f * l * mouse.vs * 0.00065 p.cursor.vy += Math.sin(mouse.a) * f * l * mouse.vs * 0.00065 } p.cursor.vx += (0 - p.cursor.x) * tension p.cursor.vy += (0 - p.cursor.y) * tension p.cursor.vx *= friction p.cursor.vy *= friction p.cursor.x += p.cursor.vx * 2 p.cursor.y += p.cursor.vy * 2 p.cursor.x = Math.min( maxCursorMove, Math.max(-maxCursorMove, p.cursor.x), ) p.cursor.y = Math.min( maxCursorMove, Math.max(-maxCursorMove, p.cursor.y), ) }) }) } function moved(point: typeof linesRef.current[0][0], withCursor = true) { const x = point.x + point.wave.x + (withCursor ? point.cursor.x : 0) const y = point.y + point.wave.y + (withCursor ? point.cursor.y : 0) return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 } } function drawLines() { const { width, height } = boundingRef.current const ctx = ctxRef.current if (!ctx) return ctx.clearRect(0, 0, width, height) ctx.beginPath() ctx.strokeStyle = lineColor linesRef.current.forEach((points) => { let p1 = moved(points[0], false) ctx.moveTo(p1.x, p1.y) points.forEach((p, idx) => { const isLast = idx === points.length - 1 p1 = moved(p, !isLast) const p2 = moved( points[idx + 1] || points[points.length - 1], !isLast, ) ctx.lineTo(p1.x, p1.y) if (isLast) ctx.moveTo(p2.x, p2.y) }) }) ctx.stroke() } function tick(t: number) { const mouse = mouseRef.current mouse.sx += (mouse.x - mouse.sx) * 0.1 mouse.sy += (mouse.y - mouse.sy) * 0.1 const dx = mouse.x - mouse.lx, dy = mouse.y - mouse.ly const d = Math.hypot(dx, dy) mouse.v = d mouse.vs += (d - mouse.vs) * 0.1 mouse.vs = Math.min(100, mouse.vs) mouse.lx = mouse.x mouse.ly = mouse.y mouse.a = Math.atan2(dy, dx) if (container) { container.style.setProperty("--x", `${mouse.sx}px`) container.style.setProperty("--y", `${mouse.sy}px`) } movePoints(t) drawLines() requestAnimationFrame(tick) } function onResize() { setSize() setLines() } function onMouseMove(e: MouseEvent) { updateMouse(e.pageX, e.pageY) } function onTouchMove(e: TouchEvent) { e.preventDefault() const touch = e.touches[0] updateMouse(touch.clientX, touch.clientY) } function updateMouse(x: number, y: number) { const mouse = mouseRef.current const b = boundingRef.current mouse.x = x - b.left mouse.y = y - b.top + window.scrollY if (!mouse.set) { mouse.sx = mouse.x mouse.sy = mouse.y mouse.lx = mouse.x mouse.ly = mouse.y mouse.set = true } } setSize() setLines() requestAnimationFrame(tick) window.addEventListener("resize", onResize) window.addEventListener("mousemove", onMouseMove) window.addEventListener("touchmove", onTouchMove, { passive: false }) return () => { window.removeEventListener("resize", onResize) window.removeEventListener("mousemove", onMouseMove) window.removeEventListener("touchmove", onTouchMove) } }, [ lineColor, backgroundColor, waveSpeedX, waveSpeedY, waveAmpX, waveAmpY, friction, tension, maxCursorMove, xGap, yGap, ]) return ( <div ref={containerRef} style={{ backgroundColor, }} className={cn( "absolute top-0 left-0 w-full h-full overflow-hidden", className, )} > <div className={cn( "absolute top-0 left-0 rounded-full animate-wave-pulse", "w-2 h-2 bg-foreground/10", )} style={{ transform: "translate3d(calc(var(--x) - 50%), calc(var(--y) - 50%), 0)", willChange: "transform", }} /> <canvas ref={canvasRef} className="block w-full h-full" /> </div> )}export type { WavesProps }
Installation
npx shadcn@latest add https://www.shadcn.io/registry/waves.json
npx shadcn@latest add https://www.shadcn.io/registry/waves.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/waves.json
bunx shadcn@latest add https://www.shadcn.io/registry/waves.json
Features
- Interactive cursor physics with waves responding to mouse movement using realistic spring dynamics
- Perlin noise generation creating smooth, natural wave patterns with organic flow algorithms
- Customizable wave properties controlling speed, amplitude, spacing, friction, and tension parameters
- Canvas-based rendering using efficient HTML5 Canvas with requestAnimationFrame optimization
- Touch-friendly interaction supporting both desktop mouse and mobile touch events
- Theme-aware styling automatically adapting to light/dark themes using CSS custom properties
- Performance optimized maintaining smooth 60fps animation across all devices
- TypeScript support with complete interface definitions for reliable integration
Examples
Subtle Waves
Loading component...
import { Waves } from '@/components/ui/shadcn-io/waves';const Example = () => ( <div className="relative w-full h-[300px] bg-white rounded-lg overflow-hidden border"> <div className="absolute inset-0"> <Waves lineColor="rgba(59, 130, 246, 0.5)" backgroundColor="white" waveSpeedX={0.005} waveSpeedY={0.002} waveAmpX={20} waveAmpY={10} friction={0.95} tension={0.002} maxCursorMove={60} xGap={8} yGap={24} /> </div> <div className="relative z-10 p-6 flex items-center justify-center h-full"> <div className="text-center"> <h3 className="text-xl font-semibold mb-1 text-black">Subtle Waves</h3> <p className="text-sm text-gray-600">Gentle motion with blue colors</p> </div> </div> </div>);export default Example;
'use client';import { useRef, useEffect } from "react"import { cn } from "@/lib/utils"interface WavesProps { /** * Color of the wave lines */ lineColor?: string /** * Background color of the container */ backgroundColor?: string waveSpeedX?: number waveSpeedY?: number waveAmpX?: number waveAmpY?: number xGap?: number yGap?: number friction?: number tension?: number maxCursorMove?: number className?: string}class Grad { x: number y: number z: number constructor(x: number, y: number, z: number) { this.x = x this.y = y this.z = z } dot2(x: number, y: number) { return this.x * x + this.y * y }}class Noise { grad3: Grad[] p: number[] perm: number[] gradP: Grad[] constructor(seed = 0) { this.grad3 = [ new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0), new Grad(-1, -1, 0), new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1), new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1), ] this.p = [ 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, ] this.perm = new Array(512) this.gradP = new Array(512) this.seed(seed) } seed(seed: number) { if (seed > 0 && seed < 1) seed *= 65536 seed = Math.floor(seed) if (seed < 256) seed |= seed << 8 for (let i = 0; i < 256; i++) { let v = i & 1 ? this.p[i] ^ (seed & 255) : this.p[i] ^ ((seed >> 8) & 255) this.perm[i] = this.perm[i + 256] = v this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12] } } fade(t: number) { return t * t * t * (t * (t * 6 - 15) + 10) } lerp(a: number, b: number, t: number) { return (1 - t) * a + t * b } perlin2(x: number, y: number) { let X = Math.floor(x), Y = Math.floor(y) x -= X y -= Y X &= 255 Y &= 255 const n00 = this.gradP[X + this.perm[Y]].dot2(x, y) const n01 = this.gradP[X + this.perm[Y + 1]].dot2(x, y - 1) const n10 = this.gradP[X + 1 + this.perm[Y]].dot2(x - 1, y) const n11 = this.gradP[X + 1 + this.perm[Y + 1]].dot2(x - 1, y - 1) const u = this.fade(x) return this.lerp( this.lerp(n00, n10, u), this.lerp(n01, n11, u), this.fade(y), ) }}export function Waves({ lineColor = "hsl(var(--foreground))", backgroundColor = "white", waveSpeedX = 0.0125, waveSpeedY = 0.005, waveAmpX = 32, waveAmpY = 16, xGap = 10, yGap = 32, friction = 0.925, tension = 0.005, maxCursorMove = 100, className,}: WavesProps) { const containerRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null) const ctxRef = useRef<CanvasRenderingContext2D | null>(null) const boundingRef = useRef({ width: 0, height: 0, left: 0, top: 0 }) const noiseRef = useRef(new Noise(Math.random())) const linesRef = useRef<Array<Array<{ x: number y: number wave: { x: number; y: number } cursor: { x: number; y: number; vx: number; vy: number } }>>>([]) const mouseRef = useRef({ x: -10, y: 0, lx: 0, ly: 0, sx: 0, sy: 0, v: 0, vs: 0, a: 0, set: false, }) useEffect(() => { const canvas = canvasRef.current const container = containerRef.current if (!canvas || !container) return ctxRef.current = canvas.getContext("2d") function setSize() { if (!container) return boundingRef.current = container.getBoundingClientRect() if (canvas) { canvas.width = boundingRef.current.width canvas.height = boundingRef.current.height } } function setLines() { const { width, height } = boundingRef.current linesRef.current = [] const oWidth = width + 200, oHeight = height + 30 const totalLines = Math.ceil(oWidth / xGap) const totalPoints = Math.ceil(oHeight / yGap) const xStart = (width - xGap * totalLines) / 2 const yStart = (height - yGap * totalPoints) / 2 for (let i = 0; i <= totalLines; i++) { const pts = [] for (let j = 0; j <= totalPoints; j++) { pts.push({ x: xStart + xGap * i, y: yStart + yGap * j, wave: { x: 0, y: 0 }, cursor: { x: 0, y: 0, vx: 0, vy: 0 }, }) } linesRef.current.push(pts) } } function movePoints(time: number) { const lines = linesRef.current const mouse = mouseRef.current const noise = noiseRef.current lines.forEach((pts) => { pts.forEach((p) => { const move = noise.perlin2( (p.x + time * waveSpeedX) * 0.002, (p.y + time * waveSpeedY) * 0.0015, ) * 12 p.wave.x = Math.cos(move) * waveAmpX p.wave.y = Math.sin(move) * waveAmpY const dx = p.x - mouse.sx, dy = p.y - mouse.sy const dist = Math.hypot(dx, dy), l = Math.max(175, mouse.vs) if (dist < l) { const s = 1 - dist / l const f = Math.cos(dist * 0.001) * s p.cursor.vx += Math.cos(mouse.a) * f * l * mouse.vs * 0.00065 p.cursor.vy += Math.sin(mouse.a) * f * l * mouse.vs * 0.00065 } p.cursor.vx += (0 - p.cursor.x) * tension p.cursor.vy += (0 - p.cursor.y) * tension p.cursor.vx *= friction p.cursor.vy *= friction p.cursor.x += p.cursor.vx * 2 p.cursor.y += p.cursor.vy * 2 p.cursor.x = Math.min( maxCursorMove, Math.max(-maxCursorMove, p.cursor.x), ) p.cursor.y = Math.min( maxCursorMove, Math.max(-maxCursorMove, p.cursor.y), ) }) }) } function moved(point: typeof linesRef.current[0][0], withCursor = true) { const x = point.x + point.wave.x + (withCursor ? point.cursor.x : 0) const y = point.y + point.wave.y + (withCursor ? point.cursor.y : 0) return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 } } function drawLines() { const { width, height } = boundingRef.current const ctx = ctxRef.current if (!ctx) return ctx.clearRect(0, 0, width, height) ctx.beginPath() ctx.strokeStyle = lineColor linesRef.current.forEach((points) => { let p1 = moved(points[0], false) ctx.moveTo(p1.x, p1.y) points.forEach((p, idx) => { const isLast = idx === points.length - 1 p1 = moved(p, !isLast) const p2 = moved( points[idx + 1] || points[points.length - 1], !isLast, ) ctx.lineTo(p1.x, p1.y) if (isLast) ctx.moveTo(p2.x, p2.y) }) }) ctx.stroke() } function tick(t: number) { const mouse = mouseRef.current mouse.sx += (mouse.x - mouse.sx) * 0.1 mouse.sy += (mouse.y - mouse.sy) * 0.1 const dx = mouse.x - mouse.lx, dy = mouse.y - mouse.ly const d = Math.hypot(dx, dy) mouse.v = d mouse.vs += (d - mouse.vs) * 0.1 mouse.vs = Math.min(100, mouse.vs) mouse.lx = mouse.x mouse.ly = mouse.y mouse.a = Math.atan2(dy, dx) if (container) { container.style.setProperty("--x", `${mouse.sx}px`) container.style.setProperty("--y", `${mouse.sy}px`) } movePoints(t) drawLines() requestAnimationFrame(tick) } function onResize() { setSize() setLines() } function onMouseMove(e: MouseEvent) { updateMouse(e.pageX, e.pageY) } function onTouchMove(e: TouchEvent) { e.preventDefault() const touch = e.touches[0] updateMouse(touch.clientX, touch.clientY) } function updateMouse(x: number, y: number) { const mouse = mouseRef.current const b = boundingRef.current mouse.x = x - b.left mouse.y = y - b.top + window.scrollY if (!mouse.set) { mouse.sx = mouse.x mouse.sy = mouse.y mouse.lx = mouse.x mouse.ly = mouse.y mouse.set = true } } setSize() setLines() requestAnimationFrame(tick) window.addEventListener("resize", onResize) window.addEventListener("mousemove", onMouseMove) window.addEventListener("touchmove", onTouchMove, { passive: false }) return () => { window.removeEventListener("resize", onResize) window.removeEventListener("mousemove", onMouseMove) window.removeEventListener("touchmove", onTouchMove) } }, [ lineColor, backgroundColor, waveSpeedX, waveSpeedY, waveAmpX, waveAmpY, friction, tension, maxCursorMove, xGap, yGap, ]) return ( <div ref={containerRef} style={{ backgroundColor, }} className={cn( "absolute top-0 left-0 w-full h-full overflow-hidden", className, )} > <div className={cn( "absolute top-0 left-0 rounded-full animate-wave-pulse", "w-2 h-2 bg-foreground/10", )} style={{ transform: "translate3d(calc(var(--x) - 50%), calc(var(--y) - 50%), 0)", willChange: "transform", }} /> <canvas ref={canvasRef} className="block w-full h-full" /> </div> )}export type { WavesProps }
Use Cases
This free open source React component works well for:
- Hero section backgrounds - Fluid wave animations for modern landing pages built with Next.js
- Interactive portfolios - Cursor-responsive effects showcasing technical capabilities using TypeScript
- Creative showcases - Organic animations for galleries and design presentations
- Dashboard interfaces - Subtle background motion for admin panels using shadcn/ui design
- Brand experiences - Fluid animations expressing innovation and creativity
- Loading screens - Mesmerizing wave patterns during content processing using Tailwind CSS animations
API Reference
Props
Prop | Type | Default | Description |
---|---|---|---|
lineColor | string | "hsl(var(--foreground))" | Color of the wave lines |
backgroundColor | string | "white" | Background color of the container |
waveSpeedX | number | 0.0125 | Horizontal wave animation speed |
waveSpeedY | number | 0.005 | Vertical wave animation speed |
waveAmpX | number | 32 | Horizontal wave amplitude |
waveAmpY | number | 16 | Vertical wave amplitude |
xGap | number | 10 | Horizontal spacing between wave lines |
yGap | number | 32 | Vertical spacing between wave points |
friction | number | 0.925 | Physics friction coefficient (0-1) |
tension | number | 0.005 | Spring tension for cursor interaction |
maxCursorMove | number | 100 | Maximum cursor influence distance |
className | string | - | Additional CSS classes |
Implementation Notes
- Component uses HTML5 Canvas API with Perlin noise algorithms for natural wave generation
- Interactive physics system responds to cursor movement with configurable spring dynamics
- Canvas automatically adapts to container dimensions and handles window resize events
- Wave properties customizable: speed (0.005-0.05), amplitude (8-64), spacing (5-50)
- Physics parameters: friction (0.8-0.99), tension (0.001-0.01), cursor influence (50-200px)
- Performance optimized using requestAnimationFrame and efficient canvas operations
- Theme integration supports CSS custom properties for automatic light/dark adaptation
- Compatible with shadcn/ui design system and works as absolute positioned background element
- Touch events supported for mobile interaction with same physics as desktop mouse
Warp
Immersive 3D warp effects with animated light beams and perspective grids. Perfect for React applications requiring futuristic backgrounds with Next.js integration and TypeScript support.
Wavy
Organic wave animations with simplex noise and customizable gradients. Perfect for React hero sections requiring fluid visual effects with Next.js integration and TypeScript support.