Skip to content
← components

Sparkle Field

new

A shimmering field of dots in a single element: a repeated dot mask intersected with an animated Perlin-noise mask via mask-composite, so the dots twinkle. No JS, no canvas.

palette
gap
40px
speed
8s

Usage

example.tsx
import { SparkleField } from "~/components/effects/sparkle-field";
<div className="h-72 overflow-hidden rounded-xl">
<SparkleField />
</div>

Two masks, intersected

The element is just a gradient. The magic is two stacked mask layers: a radial-gradient repeated into a grid of dots, and a tile of Perlin noise. mask-composite: intersect keeps only the pixels where both masks are opaque, so a dot shows only where the noise happens to be bright underneath it.

Animate the noise layer's mask-position and that bright region drifts across the grid, lighting different dots each frame. That's the twinkle, and it's one element doing it instead of thousands of animated nodes. I generate the noise with an inline SVG feTurbulence filter, so there's no image to ship.

sparkle.css
.sparkle-field {
background: var(--gradient);
/* layer two masks: the dots, then the noise */
mask-image: var(--dots), var(--noise);
mask-composite: intersect; /* keep only where both show */
animation: flicker 8s linear infinite alternate;
}
@keyframes flicker {
to { mask-position: 0 0, 120px 90px; } /* slide the noise layer */
}

Props

proptypedefaultdescription
gradientstringpurple→cyanAny CSS background — the colors the sparkles reveal.
gapnumber40Dot grid spacing in px.
speednumber8Seconds per shimmer cycle.
className / styleForwarded to the element (set a height).

Recreated from jhey.

The full implementation, across 2 files. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).

components/effects/sparkle-field.tsx
import type { CSSProperties } from "react";
import { cn } from "~/lib/utils";
export interface SparkleFieldProps {
/** Any CSS background — the colors that show through the sparkles. */
gradient?: string;
/** Grid spacing of the dots, in px. */
gap?: number;
/** Seconds for one shimmer cycle. */
speed?: number;
className?: string;
style?: CSSProperties;
}
const DEFAULT_GRADIENT =
"radial-gradient(circle at 22% 18%, #c4b5fd, transparent 55%), radial-gradient(circle at 82% 78%, #67e8f9, transparent 55%), linear-gradient(120deg, #3b82f6, #8b5cf6 52%, #ec4899)";
/**
* A shimmering field of dots in one element. A repeated radial-gradient (the
* dots) is intersected with an animated Perlin-noise mask via
* mask-composite: intersect, so only the dots under "bright" noise show, and
* sliding the noise makes them twinkle. Recreated from jhey (@jh3yy).
*/
export function SparkleField({
gradient = DEFAULT_GRADIENT,
gap = 40,
speed = 8,
className,
style,
}: SparkleFieldProps) {
return (
<div
className={cn("sparkle-field", className)}
style={
{
background: gradient,
"--sparkle-gap": `${gap}px`,
"--sparkle-speed": `${speed}s`,
...style,
} as CSSProperties
}
/>
);
}
app/styles/app.css
/* ── Sparkle field (dots × noise via mask-composite), from jhey ───────────── */
.sparkle-field {
--sparkle-gap: 40px;
--sparkle-speed: 8s;
--sparkle-dots: radial-gradient(circle at 50% 50%, #fff 2px, transparent 2.6px);
--sparkle-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.028' numOctaves='3' seed='4' stitchTiles='stitch'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='2.2' intercept='-0.25'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23n)'/%3E%3C/svg%3E");
width: 100%;
height: 100%;
-webkit-mask-image: var(--sparkle-dots), var(--sparkle-noise);
mask-image: var(--sparkle-dots), var(--sparkle-noise);
-webkit-mask-size: var(--sparkle-gap) var(--sparkle-gap), 256px 256px;
mask-size: var(--sparkle-gap) var(--sparkle-gap), 256px 256px;
-webkit-mask-position: 0 0, 0 0;
mask-position: 0 0, 0 0;
-webkit-mask-repeat: repeat, repeat;
mask-repeat: repeat, repeat;
-webkit-mask-composite: source-in;
mask-composite: intersect;
animation: sparkle-flicker var(--sparkle-speed) linear infinite alternate;
}
@keyframes sparkle-flicker {
to {
-webkit-mask-position: 0 0, 120px 90px;
mask-position: 0 0, 120px 90px;
}
}
@media (prefers-reduced-motion: reduce) {
.sparkle-field { animation: none; }
}