A lightweight SVG dot grid background with an optional radial fade and a few softly pulsing glow dots. One pattern element, token colors, hydration-safe, still under reduced motion.
Small details, shipped weekly.
Every Friday: fixes, polish and one thing you asked for.
import { DotPattern } from "@/components/ballmac/dot-pattern"
export default function DotPatternDemo() {
return (
<div className="relative isolate flex h-80 w-full max-w-2xl flex-col items-center justify-center overflow-hidden rounded-xl border bg-background px-6 text-center">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_50%_45%_at_50%_45%,color-mix(in_oklch,var(--chart-1)_10%,transparent),transparent)]"
/>
<DotPattern glow glowCount={26} width={14} height={14} radius={1.1} className="-z-10" />
<span className="font-mono text-xs tracking-widest text-muted-foreground uppercase">Changelog</span>
<h2 className="mt-3 max-w-md text-4xl font-semibold tracking-tight text-balance sm:text-5xl">
Small details, shipped weekly.
</h2>
<p className="mt-3 max-w-sm text-sm text-muted-foreground">
Every Friday: fixes, polish and one thing you asked for.
</p>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/dot-patternCopy the source into your project.
components/ballmac/dot-pattern.tsx// Ballmac UI: Dot Pattern. https://ui.ballmac.com/components/dot-pattern "use client" import * as React from "react" import { cn } from "@/lib/utils" type DotPatternProps = Omit<React.ComponentProps<"div">, "children"> & { /** Horizontal spacing between dots in pixels. */ width?: number /** Vertical spacing between dots in pixels. */ height?: number /** Dot radius in pixels. */ radius?: number /** Make a few random dots glow and pulse softly. */ glow?: boolean /** How many dots glow at once when `glow` is on. */ glowCount?: number /** Color of the glowing dots. Any CSS color or var(). */ glowColor?: string /** Fade the pattern out toward the edges with a radial mask. */ radialMask?: boolean } type GlowDot = { id: number; x: number; y: number; delay: number; duration: number } function DotPattern({ width = 16, height = 16, radius = 1, glow = false, glowCount = 18, glowColor = "var(--chart-1)", radialMask = true, className, style, ...props }: DotPatternProps) { const rootRef = React.useRef<HTMLDivElement>(null) const patternId = `bm-dots-${React.useId().replace(/[^a-zA-Z0-9_-]/g, "")}` // Glow positions are random, so they're picked on the client after mount (hydration-safe). const [dots, setDots] = React.useState<GlowDot[]>([]) React.useEffect(() => { const root = rootRef.current if (!root || !glow) { setDots([]) return } const pick = () => { const cols = Math.floor(root.clientWidth / width) const rows = Math.floor(root.clientHeight / height) if (cols < 1 || rows < 1) return setDots( Array.from({ length: glowCount }, (_, id) => ({ id, x: (Math.floor(Math.random() * cols) + 0.5) * width, y: (Math.floor(Math.random() * rows) + 0.5) * height, delay: Math.random() * 4, duration: 2.5 + Math.random() * 3, })) ) } pick() const ro = new ResizeObserver(pick) ro.observe(root) return () => ro.disconnect() }, [glow, glowCount, width, height]) React.useEffect(() => { const root = rootRef.current if (!root || !dots.length || typeof root.animate !== "function") return if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return const animations = Array.from(root.querySelectorAll<SVGCircleElement>("[data-slot=dot-pattern-glow]")).map( (node, i) => node.animate([{ opacity: 0.15 }, { opacity: 1 }, { opacity: 0.15 }], { duration: dots[i]!.duration * 1000, delay: -dots[i]!.delay * 1000, iterations: Infinity, easing: "ease-in-out", }) ) let visible = true const sync = () => { for (const a of animations) { if (visible && !document.hidden) a.play() else a.pause() } } const io = new IntersectionObserver(([entry]) => { visible = entry?.isIntersecting ?? true sync() }) io.observe(root) document.addEventListener("visibilitychange", sync) return () => { io.disconnect() document.removeEventListener("visibilitychange", sync) for (const a of animations) a.cancel() } }, [dots]) const mask = radialMask ? "radial-gradient(ellipse 70% 65% at 50% 50%, black 25%, transparent 80%)" : undefined return ( <div ref={rootRef} aria-hidden="true" data-slot="dot-pattern" className={cn( "pointer-events-none absolute inset-0 overflow-hidden [--dot:color-mix(in_oklch,var(--foreground)_30%,transparent)]", className )} style={{ maskImage: mask, WebkitMaskImage: mask, ...style }} {...props} > <svg className="absolute inset-0 size-full"> <defs> <pattern id={patternId} width={width} height={height} patternUnits="userSpaceOnUse"> <circle cx={width / 2} cy={height / 2} r={radius} style={{ fill: "var(--dot)" }} /> </pattern> </defs> <rect width="100%" height="100%" fill={`url(#${patternId})`} /> {dots.map((dot) => ( <circle key={dot.id} data-slot="dot-pattern-glow" cx={dot.x} cy={dot.y} r={radius * 1.8} style={{ fill: glowColor, opacity: 0.6, filter: `drop-shadow(0 0 ${radius * 4}px ${glowColor})`, }} /> ))} </svg> </div> ) } export { DotPattern, type DotPatternProps }Update the import paths to match your project setup.
Usage
import { DotPattern } from "@/components/ballmac/dot-pattern"The full example is in the Code tab above.
Examples
Dense, no glow
Empty inbox
Nothing needs your attention right now.
import { DotPattern } from "@/components/ballmac/dot-pattern"
export default function DotPatternDense() {
return (
<div className="relative isolate h-56 w-full max-w-md overflow-hidden rounded-xl border bg-card">
<DotPattern width={10} height={10} radius={0.75} radialMask={false} className="-z-10 [mask-image:linear-gradient(to_bottom,black,transparent)]" />
<div className="p-6">
<p className="text-sm font-medium">Empty inbox</p>
<p className="mt-1 text-sm text-muted-foreground">Nothing needs your attention right now.</p>
</div>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
widthHorizontal spacing between dots in pixels. | number | 16 |
heightVertical spacing between dots in pixels. | number | 16 |
radiusDot radius in pixels. | number | 1 |
glowMake a few random dots glow and pulse softly. | boolean | false |
glowCountHow many dots glow at once when `glow` is on. | number | 18 |
glowColorColor of the glowing dots. Any CSS color or var(). | string | "var(--chart-1)" |
radialMaskFade the pattern out toward the edges with a radial mask. | boolean | true |
Also accepts the standard attributes of its root element.
Use with AI
Place <DotPattern /> as the first child of a relative, overflow-hidden container (absolute inset-0, aria-hidden, pointer-events-none). Add glow for a few pulsing accent dots. The cheapest Ballmac background; safe for large areas. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Dot Pattern (@ballmac/dot-pattern) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Quiet texture behind heroes, feature cards, empty states and auth pages
- Large page areas where canvas effects would be too heavy
Not for
- When you want visible motion as the focal point (use flickering-grid or beams-background)
Registry JSON: https://ui.ballmac.com/r/dot-pattern.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- shadcn/utils
Pairs well with
Orbiting Circles
Items that orbit a center on circular tracks while staying upright. Evenly spaced, any radius, speed and direction; stack several for rings. Paused off-screen, static under reduced motion.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.
Spotlight Card
A card whose surface and border pick up a soft glow that follows the pointer, driven by CSS variables with no re-render per move. Keyboard focus shows a steady glow.
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.