A hyperspace tunnel of streaks flying out from the center on a canvas, in theme colors with a soft vignette for readable content, parallax toward the pointer, pausing off screen.
Launching soon
Light speed, no jank
Join the waitlist to get early access.
import { WarpBackground } from "@/components/ballmac/warp-background"
export default function WarpBackgroundDemo() {
return (
<WarpBackground className="h-80 w-full max-w-2xl rounded-2xl border" contentClassName="flex h-80 flex-col items-center justify-center gap-3 px-6 text-center">
<p className="font-mono text-xs tracking-widest text-muted-foreground uppercase">Launching soon</p>
<h3 className="text-4xl font-semibold tracking-tight sm:text-5xl">Light speed, no jank</h3>
<p className="max-w-xs text-sm text-muted-foreground">Join the waitlist to get early access.</p>
</WarpBackground>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/warp-backgroundAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/colorCopy the source into your project.
components/ballmac/warp-background.tsx// Ballmac UI: Warp Background. https://ui.ballmac.com/components/warp-background "use client" import * as React from "react" import { cn } from "@/lib/utils" import { observeTheme, resolveCssColor } from "@/lib/ballmac/color" type WarpBackgroundProps = Omit<React.ComponentProps<"div">, "color"> & { /** How many streaks fly past at once. */ density?: number /** How fast they travel. 1 is a steady cruise. */ speed?: number /** Colors of the streaks: CSS variable names such as "--chart-1", or any CSS colors. Streaks pick from these at random. */ colors?: string[] /** Darken the middle so content on top stays readable. */ vignette?: boolean /** The vanishing point follows the pointer a little. */ parallax?: boolean /** Classes for the layer that holds the children. */ contentClassName?: string } type Star = { angle: number; radius: number; speed: number; color: number; length: number } /** A hyperspace tunnel: streaks fly outward from the center on a canvas, with your content on top. */ function WarpBackground({ density = 240, speed = 1, colors = ["--foreground", "--chart-1", "--chart-4"], vignette = true, parallax = true, className, contentClassName, children, ...props }: WarpBackgroundProps) { const rootRef = React.useRef<HTMLDivElement>(null) const canvasRef = React.useRef<HTMLCanvasElement>(null) const colorKey = colors.join("|") React.useEffect(() => { const root = rootRef.current const canvas = canvasRef.current const ctx = canvas?.getContext("2d") if (!root || !canvas || !ctx) return const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches let width = 0 let height = 0 let dpr = 1 let palette = colors.map((c) => resolveCssColor(root, c)) let stars: Star[] = [] let cx = 0.5 let cy = 0.5 let targetX = 0.5 let targetY = 0.5 let frame = 0 let last = 0 let visible = true const spawn = (initial: boolean): Star => ({ angle: Math.random() * Math.PI * 2, radius: initial ? Math.random() ** 2 * 0.9 : 0.01 + Math.random() * 0.03, speed: 0.25 + Math.random() * 0.6, color: Math.floor(Math.random() * palette.length), length: 0.6 + Math.random() * 1.4, }) const resize = () => { dpr = Math.min(window.devicePixelRatio || 1, 2) width = root.clientWidth height = root.clientHeight canvas.width = Math.round(width * dpr) canvas.height = Math.round(height * dpr) } const draw = (dt: number) => { ctx.setTransform(dpr, 0, 0, dpr, 0, 0) ctx.clearRect(0, 0, width, height) const reach = Math.hypot(width, height) / 2 const ox = cx * width const oy = cy * height ctx.lineCap = "round" for (let i = 0; i < stars.length; i++) { const s = stars[i]! const before = s.radius s.radius += dt * speed * s.speed * (0.12 + s.radius * 1.9) if (s.radius * reach > reach * 1.1) { stars[i] = spawn(false) continue } const tail = Math.max(before, s.radius - dt * speed * s.speed * (0.12 + s.radius * 1.9) * s.length * 3) const cos = Math.cos(s.angle) const sin = Math.sin(s.angle) ctx.globalAlpha = Math.min(1, s.radius * 2.2) * 0.9 ctx.strokeStyle = palette[s.color % palette.length]! ctx.lineWidth = 0.6 + s.radius * 2.2 ctx.beginPath() ctx.moveTo(ox + cos * tail * reach, oy + sin * tail * reach) ctx.lineTo(ox + cos * s.radius * reach, oy + sin * s.radius * reach) ctx.stroke() } ctx.globalAlpha = 1 } const loop = (now: number) => { const dt = last ? Math.min((now - last) / 1000, 0.05) : 0 last = now cx += (targetX - cx) * 0.06 cy += (targetY - cy) * 0.06 draw(dt) frame = requestAnimationFrame(loop) } const sync = () => { cancelAnimationFrame(frame) last = 0 if (reduceMotion) draw(0.016 * 8) else if (visible && !document.hidden) frame = requestAnimationFrame(loop) } const onMove = (e: PointerEvent) => { if (!parallax || reduceMotion) return const box = root.getBoundingClientRect() targetX = 0.5 + ((e.clientX - box.left) / box.width - 0.5) * 0.14 targetY = 0.5 + ((e.clientY - box.top) / box.height - 0.5) * 0.14 } resize() stars = Array.from({ length: density }, () => spawn(true)) sync() root.addEventListener("pointermove", onMove) const ro = new ResizeObserver(() => { resize() if (reduceMotion) draw(0.1) }) ro.observe(root) const io = new IntersectionObserver(([entry]) => { visible = entry?.isIntersecting ?? true sync() }) io.observe(root) const stopTheme = observeTheme(() => { palette = colors.map((c) => resolveCssColor(root, c)) if (reduceMotion) draw(0.1) }) document.addEventListener("visibilitychange", sync) return () => { cancelAnimationFrame(frame) root.removeEventListener("pointermove", onMove) ro.disconnect() io.disconnect() stopTheme() document.removeEventListener("visibilitychange", sync) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [density, speed, colorKey, parallax]) return ( <div ref={rootRef} data-slot="warp-background" className={cn("relative isolate overflow-hidden bg-background", className)} {...props}> <canvas ref={canvasRef} aria-hidden="true" className="pointer-events-none absolute inset-0 size-full" /> {vignette && ( <span aria-hidden="true" className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_45%_45%_at_50%_50%,var(--background),transparent_75%)] opacity-80" /> )} <div className={cn("relative z-10", contentClassName)}>{children}</div> </div> ) } export { WarpBackground, type WarpBackgroundProps }Update the import paths to match your project setup.
Usage
import { WarpBackground } from "@/components/ballmac/warp-background"The full example is in the Code tab above.
Examples
Calm, dense variant
Edge network
Fewer, slower streaks keep dense content calm.
Instant rollbacks
Fewer, slower streaks keep dense content calm.
import { WarpBackground } from "@/components/ballmac/warp-background"
export default function WarpBackgroundCards() {
return (
<WarpBackground density={90} speed={0.5} colors={["--foreground", "--chart-2"]} parallax={false} className="w-full max-w-xl rounded-2xl border p-6" contentClassName="grid gap-3 sm:grid-cols-2">
{["Edge network", "Instant rollbacks"].map((t) => (
<div key={t} className="rounded-xl border bg-card/80 p-4 backdrop-blur">
<p className="text-sm font-semibold">{t}</p>
<p className="mt-1 text-[13px] text-muted-foreground">Fewer, slower streaks keep dense content calm.</p>
</div>
))}
</WarpBackground>
)
}API reference
| Prop | Type | Default |
|---|---|---|
densityHow many streaks fly past at once. | number | 240 |
speedHow fast they travel. 1 is a steady cruise. | number | 1 |
colorsColors of the streaks: CSS variable names such as "--chart-1", or any CSS colors. Streaks pick from these at random. | string[] | ["--foreground", "--chart-1", "--chart-4"] |
vignetteDarken the middle so content on top stays readable. | boolean | true |
parallaxThe vanishing point follows the pointer a little. | boolean | true |
contentClassNameClasses for the layer that holds the children. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Screen readers | The canvas is aria-hidden; children are read normally |
| Contrast | The vignette darkens the center behind text |
| Reduced motion | A single still frame of streaks |
Use with AI
<WarpBackground density speed colors vignette parallax>content</WarpBackground>. colors takes CSS variable names. The content sits on its own layer above the canvas. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Warp Background (@ballmac/warp-background) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Launch and waitlist pages
- A dramatic backdrop for one centered message
Not for
- Content-heavy pages
- Anything that must be calm all the time
Registry JSON: https://ui.ballmac.com/r/warp-background.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- @ballmac/colorshadcn/utils
Pairs well with
Particles
A canvas field of softly twinkling particles with depth, gentle drift and a pointer that attracts or repels them. Token colors, DPR aware, pauses off-screen, still under reduced motion.
Retro Grid
A perspective floor of grid lines gliding toward you under a glowing horizon, tinted with any theme color, tilt and cell size adjustable, still under reduced motion.
Light Rays
Soft beams of theme-colored light fanning down from the top edge, each swaying and breathing at its own pace, deterministic so the server and browser match.
Aurora Background
Soft, slowly drifting aurora light made of blurred token-colored glows with fine vertical curtains and an optional radial fade. Pure CSS layers, paused off-screen, still under reduced motion.