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.
↓Scroll to step through it
Fill, clip, together
AuroraText is a span with a 200%-wide rainbow painted behind the letters,
clipped so the gradient shows only through the glyphs. One CSS keyframe slides
background-position back and forth. It uses no canvas, SVG filter, or font
trick.
The component renders two nested spans inside a relative wrapper. The first
is sr-only and holds 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 has no visible seam.
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.
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 sets it to 10 / speed).
Nothing else moves: no transform, no opacity pulse, and no layout work.
Only the paint position of an existing gradient changes. Because the
stops loop back to the first color, the turnaround at either end reads as
continuous drift rather than a hard bounce.
Cheap when idle
A forever-running background-position animation is main-thread paint work.
AuroraText pauses it off-screen. An IntersectionObserver
with rootMargin: "128px" watches the gradient span and flips
animationPlayState as soon as it clears that margin: paused outside,
running again on the way back in, with the timeline intact.
Clearing animationPlayState (empty string) restores the stylesheet
default without restarting 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.
The result
A clipped gradient, one background-position keyframe, an observer that pauses the animation off-screen, and an sr-only twin that keeps the words readable. That is the whole aurora.
gradient fill
clip only
together
- Gradient
- 135° linear-gradient, looped stops
- Clip
- bg-clip-text + text-transparent
- Together
- gradient visible only through glyphs
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>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.
gradient fill
clip only
together
- Gradient
- 135° linear-gradient, looped stops
- Clip
- bg-clip-text + text-transparent
- Together
- gradient visible only through glyphs
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>Fill, clip, together
AuroraText is a span with a 200%-wide rainbow painted behind the letters,
clipped so the gradient shows only through the glyphs. One CSS keyframe slides
background-position back and forth. It uses no canvas, SVG filter, or font
trick.
The component renders two nested spans inside a relative wrapper. The first
is sr-only and holds 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 has no visible seam.
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.
bg-[length:200%_auto] · animate-aurora-text
- Gradient
- 200% wide, stops looped to first
- Position
- only property that moves
@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;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 sets it to 10 / speed).
Nothing else moves: no transform, no opacity pulse, and no layout work.
Only the paint position of an existing gradient changes. Because the
stops loop back to the first color, the turnaround at either end reads as
continuous drift rather than a hard bounce.
animationPlayState flips to “paused” outside rootMargin 128px and resumes where it left off on scroll-in
- Playing
- intersecting: animationPlayState empty
- Paused
- off-screen: animationPlayState paused
const io = new IntersectionObserver(([entry]) => { el.style.animationPlayState = entry.isIntersecting ? "" : "paused";},{ rootMargin: "128px" },);io.observe(el);Cheap when idle
A forever-running background-position animation is main-thread paint work.
AuroraText pauses it off-screen. An IntersectionObserver
with rootMargin: "128px" watches the gradient span and flips
animationPlayState as soon as it clears that margin: paused outside,
running again on the way back in, with the timeline intact.
Clearing animationPlayState (empty string) restores the stylesheet
default without restarting 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.
Ship beautiful UI
The result
A clipped gradient, one background-position keyframe, an observer that pauses the animation off-screen, and an sr-only twin that keeps the words readable. That is 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 doesn't move. There is no slower fallback and no partial sweep.
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
background-positionGradient text continuous keyframe loop