Sparkle Text
A short headline cast in top-lit chrome, wrapped in a soft accent bloom, with four-point sparkles twinkling on their own offbeat timers.
500
Three layers share one glyph: a blurred halo behind, a metallic clipped fill lit from above, and a sparkle overlay. The chrome ramp and glow tint toward a single accent, so one prop re-themes the whole thing. The sparkle field is seeded, so it renders identically on server and client. Best on short strings: a milestone number, a launch word.
Installation
Complete the shared Setup first, then copy the component into
components/ui/sparkle-text.tsx.
'use client';import * as React from 'react';import { cn } from '@/lib/cn';function seeded(n: number) { const x = Math.sin(n * 12.9898) * 43758.5453; return x - Math.floor(x);}type Tone = 'bright' | 'accent' | 'second';type Star = { left: number; top: number; size: number; glow: number; delay: number; dur: number; tone: Tone;};const HALO_CX = 50;const HALO_CY = 46;const HALO_HX = 58;const HALO_HY = 50;function buildStars(count: number, seed: number): Star[] { const stars: Star[] = []; for (let i = 0; i < count; i++) { const n = i + seed * 100; const jitter = (seeded(n * 2.3) - 0.5) * (1.5 / count); const angle = (i / count + jitter) * Math.PI * 2 - Math.PI / 2; const radius = 1.05 + seeded(n * 3.1) * 0.3; const left = HALO_CX + Math.cos(angle) * HALO_HX * radius; const top = HALO_CY + Math.sin(angle) * HALO_HY * radius; const size = 22 + seeded(n * 1.7) * 26; const t = seeded(n * 8.1); const tone: Tone = t < 0.4 ? 'bright' : t < 0.72 ? 'accent' : 'second'; stars.push({ left, top, size, glow: size * 0.5, delay: seeded(n * 4.9) * 2.6, dur: 1.8 + seeded(n * 5.7) * 1.8, tone, }); } return stars;}const SPARKLE_PATH = 'M12 0C12.9 7.6 16.4 11.1 24 12C16.4 12.9 12.9 16.4 12 24C11.1 16.4 7.6 12.9 0 12C7.6 11.1 11.1 7.6 12 0Z';export type SparkleTextProps = { children: React.ReactNode; accent?: string; secondary?: string; count?: number; bloom?: number; glow?: number; seed?: number; className?: string;};export function SparkleText({ children, accent = '#a78bfa', secondary, count = 7, bloom = 22, glow = 0.95, seed = 0, className,}: SparkleTextProps) { const stars = React.useMemo(() => buildStars(count, seed), [count, seed]); const vars = { '--sparkle-accent': accent, ...(secondary ? { '--sparkle-accent-2': secondary } : {}), '--sparkle-bloom-blur': `${bloom}px`, '--sparkle-bloom-strength': glow, } as React.CSSProperties; const glyphClass = cn( 'text-[64px] font-bold tracking-[-0.03em] sm:text-[88px]', className, ); return ( <span className={cn('sparkle-text', glyphClass)} style={vars}> <span className="sparkle-bloom" aria-hidden> {children} </span> <span className="sparkle-fill">{children}</span> <span className="sparkle-stars" aria-hidden> {stars.map((s, i) => ( <span key={i} className="sparkle-star" data-tone={s.tone} style={ { left: `${s.left}%`, top: `${s.top}%`, width: s.size, height: s.size, '--sparkle-glow': `${s.glow}px`, '--sparkle-delay': `${s.delay}s`, '--sparkle-dur': `${s.dur}s`, } as React.CSSProperties } > <svg viewBox="0 0 24 24" fill="currentColor"> <path d={SPARKLE_PATH} /> </svg> </span> ))} </span> </span> );}Add the styles to your global.css.
.sparkle-text {
--sparkle-accent: #a78bfa;
--sparkle-accent-2: color-mix(in srgb, var(--sparkle-accent) 42%, #ffffff);
--sparkle-bloom-blur: 20px;
--sparkle-bloom-strength: 0.85;
position: relative;
display: inline-grid;
isolation: isolate;
line-height: 0.9;
}
.sparkle-text > .sparkle-fill,
.sparkle-text > .sparkle-bloom {
grid-area: 1 / 1;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.sparkle-text > .sparkle-fill {
z-index: 2;
background-image: linear-gradient(
to bottom,
#ffffff 0%,
color-mix(in srgb, var(--sparkle-accent) 24%, #ffffff) 34%,
color-mix(in srgb, var(--sparkle-accent) 62%, #f4f0ff) 70%,
color-mix(in srgb, var(--sparkle-accent) 42%, #ffffff) 100%
);
text-shadow:
0 -1px 0 rgba(255, 255, 255, 0.55),
0 1px 0 color-mix(in srgb, var(--sparkle-accent) 40%, #ffffff),
0 2px 0 color-mix(in srgb, var(--sparkle-accent) 78%, #8b7ac0),
0 3px 0 color-mix(in srgb, var(--sparkle-accent) 65%, #6656a0),
0 6px 16px color-mix(in srgb, var(--sparkle-accent) 45%, transparent),
0 2px 3px rgba(0, 0, 0, 0.18);
}
:where(html:not(.dark)) .sparkle-text > .sparkle-fill {
background-image: linear-gradient(
to bottom,
color-mix(in srgb, var(--sparkle-accent) 55%, #ffffff) 0%,
color-mix(in srgb, var(--sparkle-accent) 72%, #5b4a95) 44%,
color-mix(in srgb, var(--sparkle-accent) 60%, #3e3168) 78%,
color-mix(in srgb, var(--sparkle-accent) 78%, #6f5eaa) 100%
);
text-shadow:
0 -1px 0 rgba(255, 255, 255, 0.65),
0 1px 0 color-mix(in srgb, var(--sparkle-accent) 55%, #2c2358),
0 2px 0 color-mix(in srgb, var(--sparkle-accent) 48%, #241c48),
0 3px 0 color-mix(in srgb, var(--sparkle-accent) 42%, #1f1740),
0 5px 10px rgba(46, 34, 88, 0.32);
}
.sparkle-text > .sparkle-bloom {
z-index: -1;
background-image: linear-gradient(var(--sparkle-accent), var(--sparkle-accent));
filter: blur(var(--sparkle-bloom-blur));
opacity: var(--sparkle-bloom-strength);
text-shadow: 0 0 28px var(--sparkle-accent);
animation: sparkle-breathe 5.5s ease-in-out infinite;
}
.dark .sparkle-text > .sparkle-bloom {
mix-blend-mode: plus-lighter;
}
@keyframes sparkle-breathe {
0%,
100% {
opacity: calc(var(--sparkle-bloom-strength) * 0.78);
}
50% {
opacity: var(--sparkle-bloom-strength);
}
}
.sparkle-text > .sparkle-stars {
position: absolute;
inset: 0;
z-index: 1;
overflow: visible;
pointer-events: none;
}
.sparkle-star {
position: absolute;
color: var(--sparkle-accent);
filter: drop-shadow(0 0 var(--sparkle-glow, 6px) var(--sparkle-accent));
transform: translate(-50%, -50%) scale(0.25);
opacity: 0;
animation: sparkle-twinkle var(--sparkle-dur, 2.4s) ease-in-out
var(--sparkle-delay, 0s) infinite;
will-change: transform, opacity;
}
.dark .sparkle-star {
color: #ffffff;
}
.dark .sparkle-star[data-tone='accent'] {
color: color-mix(in srgb, var(--sparkle-accent) 82%, #ffffff);
}
.dark .sparkle-star[data-tone='second'] {
color: var(--sparkle-accent-2);
}
:where(html:not(.dark)) .sparkle-star[data-tone='accent'] {
color: color-mix(in srgb, var(--sparkle-accent) 70%, #000000);
}
:where(html:not(.dark)) .sparkle-star[data-tone='second'] {
color: color-mix(in srgb, var(--sparkle-accent-2) 60%, #000000);
}
.sparkle-star svg {
display: block;
width: 100%;
height: 100%;
}
@keyframes sparkle-twinkle {
0%,
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.2) rotate(0deg);
}
45% {
opacity: 1;
transform: translate(-50%, -50%) scale(1) rotate(30deg);
}
70% {
opacity: 0.35;
transform: translate(-50%, -50%) scale(0.7) rotate(45deg);
}
}
@media (prefers-reduced-motion: reduce) {
.sparkle-text > .sparkle-bloom {
animation: none;
}
.sparkle-star {
animation: none;
opacity: 0.85;
transform: translate(-50%, -50%) scale(1) rotate(18deg);
}
}Usage
import { SparkleText } from '@/components/ui/sparkle-text';
export default function Example() {
return <SparkleText>500</SparkleText>;
}Examples
Any accent, and a companion hue for a two-tone scatter.
shipped
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The text to cast in chrome. |
accent | string | "#a78bfa" | Bloom + sparkle color, mixed into the chrome. |
secondary | string | - | A second sparkle hue for the two-tone scatter. |
count | number | 7 | How many sparkles to scatter. |
bloom | number | 22 | Halo blur in px. |
glow | number | 0.95 | Halo opacity, 0 to 1. |
seed | number | 0 | Shifts the scatter to a different layout. |
className | string | - | Sets size, weight, and tracking on the glyphs. |