MynduiMyndui
0

Anatomy of Aurora Text

How a looping linear-gradient becomes drifting rainbow text via background-clip, and why the animation pauses the moment it leaves the viewport.

01/04Fill, clip, together

gradient fill

clip only

together

Gradient
135° linear-gradient, looped stops
Clip
bg-clip-text + text-transparent
Together
gradient visible only through glyphs
tsx
const stops = [...colors, colors[0]].join(", ");<span className="sr-only">{children}</span><spanaria-hidden="true"className="animate-aurora-text bg-[length:200%_auto] bg-clip-text text-transparent motion-reduce:animate-none"style={{  backgroundImage: `linear-gradient(135deg, ${stops})`,  "--aurora-text-speed": `${10 / speed}s`,}}>{children}</span>
01

Fill, clip, together

AuroraText isn't a canvas, a SVG filter, or a font trick. It's a span with a 200%-wide rainbow painted behind the letters, clipped so you only see the gradient through the glyphs — then one CSS keyframe that slides background-position back and forth. Here's the whole stack.

The component renders two nested spans inside a relative wrapper. The first is sr-only — the real children for screen readers. The second is aria-hidden and carries the aurora: a linear-gradient(135deg, …) with every stop listed, then the first color repeated so the loop seams cleanly.

bg-clip-text plus text-transparent is the whole clip. The gradient fills a box twice as wide as the text; only the letter shapes stay visible. Default stops are the system rainbow — #ff2d55 through #bf5af2 — but any colors array works the same way.

02/04The background-position sweep

bg-[length:200%_auto] · animate-aurora-text

Gradient
200% wide, stops looped to first
Position
only property that moves
tsx
@keyframes aurora-text {0%   { background-position: 0% 50%; }50%  { background-position: 100% 50%; }100% { background-position: 0% 50%; }}--animate-aurora-text: aurora-text var(--aurora-text-speed, 10s) infinite linear;
02

The background-position sweep

The motion is one keyframe. aurora-text walks background-position from 0% 50% to 100% 50% and back, linear infinite, timed by --aurora-text-speed (defaults to 10s; speed is just 10 / speed).

Nothing else moves. No transform, no opacity pulse, no layout thrash — just the paint position of a gradient that's already there. Because the stops loop back to the first color, the turnaround at either end reads as continuous drift rather than a hard bounce.

03/04Cheap when idle
in view
128px past edge

animationPlayState flips to “paused” outside rootMargin 128px — resumes seamlessly on scroll-in

Playing
intersecting — animationPlayState empty
Paused
off-screen — animationPlayState paused
tsx
const io = new IntersectionObserver(([entry]) => {  el.style.animationPlayState = entry.isIntersecting ? "" : "paused";},{ rootMargin: "128px" },);io.observe(el);
03

Cheap when idle

A forever-running background-position animation is main-thread paint work. AuroraText doesn't leave it spinning off-screen. An IntersectionObserver with rootMargin: "128px" watches the gradient span and flips animationPlayState the moment it clears that margin — paused outside, running again on the way back in, timeline intact.

Clearing animationPlayState (empty string) restores the stylesheet default — it doesn't restart the keyframe, so scroll-in resumes wherever the aurora left off. The 128px pad means the sweep is already moving before the text fully enters the viewport.

04/04Result

Ship beautiful UI

04

The result

A clipped gradient, one background-position keyframe, an observer that kills the cost when you're not looking, and a sr-only twin so the light show never replaces the words. That's the whole aurora.

Accessibility

Two spans, two jobs. Screen readers hit the sr-only copy and never see the decorative twin (aria-hidden). Sighted users who prefer reduced motion get motion-reduce:animate-none on the gradient span — the rainbow still paints, but it sits still. No slower fallback, no partial sweep.

tsx
 
className="animate-aurora-text … motion-reduce:animate-none"

The observer still mounts under reduced motion; pausing a no-op animation is harmless, and removing the effect would just add a branch for no gain.

Motion Score

Aurora TextCCPaint-triggering
Cbackground-positionGradient text continuous keyframe loop
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →