liten

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.

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.

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

Example.tsx
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

PropTypeDefaultDescription
childrenReact.ReactNode-The text to cast in chrome.
accentstring"#a78bfa"Bloom + sparkle color, mixed into the chrome.
secondarystring-A second sparkle hue for the two-tone scatter.
countnumber7How many sparkles to scatter.
bloomnumber22Halo blur in px.
glownumber0.95Halo opacity, 0 to 1.
seednumber0Shifts the scatter to a different layout.
classNamestring-Sets size, weight, and tracking on the glyphs.
On this page0%