ShinyText

Configurable shimmer sweep for text.

Live demo

01

A single CSS keyframe slides a brighter band across each text element. Each section pairs a different colour and duration β€” the captions list the exact baseColor and shineColor so you can copy them directly.

Premium Β· silver shine on charcoal

The default look β€” a neutral grey resting state with a near-white sweep. Works on any light or dark surface.

Premium
baseColor="#94a3b8" Β· shineColor="#ffffff" Β· duration=3

Gold on charcoal Β· luxury CTA

Warm gold sweeping over a near-white base sat inside a deep gradient button. Faster duration (2.5s) keeps the eye anchored on the call-to-action.

baseColor="#ffffff" Β· shineColor="#fbbf24" Β· duration=2.5

Brand blue Β· sky shine on slate

A cool brand-leaning combination. The slate base reads as static type until the cyan band passes through.

Build with us
baseColor="#475569" Β· shineColor="#0ea5e9" Β· duration=3

Success green Β· status badge

Emerald shine on a darker green base β€” perfect for "live" or "operational" indicators.

ALL SYSTEMS GO
baseColor="#065f46" Β· shineColor="#34d399" Β· duration=2.2

Danger red Β· attention copy

Bright crimson shine over a maroon base. Fast duration (1.6s) creates urgency without feeling aggressive.

LIMITED TIME
baseColor="#7f1d1d" Β· shineColor="#f87171" Β· duration=1.6

Dark mode Β· light sweep on midnight

On dark surfaces, an off-white base with a pure-white shine reads beautifully. Slower duration (5s) feels meditative.

extraordinary
baseColor="#cbd5e1" Β· shineColor="#ffffff" Β· duration=5

Mono purple Β· fast loop Β· 1.5s

A high-energy violet shine over slate. The shorter duration suits hero animations where attention is the goal.

Move fast.
baseColor="#1e293b" Β· shineColor="#a78bfa" Β· duration=1.5

Direction Β· lr vs rl Β· same colours, mirrored sweep

Toggling direction between lr and rl reverses the sweep without altering the gradient β€” useful for arrow indicators.

lr β†’
rl ←
direction="lr" Β· direction="rl"

Staged delays Β· loop=false Β· run once in sequence

Three separate ShinyText instances with staggered delay and loop=false. Each plays exactly one shine then settles.

Ready. Set. Go.

Implementation

02
ShinyText.svelte
<script lang="ts">
  import ShinyText from '$lib/components/ShinyText.svelte';
</script>
​
<ShinyText text="Premium" baseColor="#475569" shineColor="#fbbf24" duration={2.5} />

ShinyText paints a CSS gradient over the text, then animates a brighter highlight band sweeping across the gradient using a single keyframe block. There is zero JavaScript at runtime β€” the animation is GPU-composited and is suppressed automatically when prefers-reduced-motion: reduce is set.

Logic explainer

03

What Does It Do? (Plain English)

ShinyText takes a plain string and runs a bright "shine" band across the letters on a loop, like a flashlight sweeping over engraved metal. The base text sits in a muted colour; a brighter band moves left-to-right (or right-to-left) and falls off back to the muted base on either side.

It is pure CSS β€” one linear-gradient clipped to the letter shapes via background-clip: text, animated by sliding background-position. No JavaScript runs after the component mounts. Think of it as polish for a CTA: more refined than a colour change, less heavy than a bouncy attention-grabber.

How It Works (Pseudo-Code)

state:
  none β€” every behaviour is encoded in CSS custom properties

derive:
  gradient        = "linear-gradient(90deg, base 0%, shine 50%, base 100%)"
  animDirection   = direction === 'lr' ? 'normal' : 'reverse'
  iterationCount  = loop ? 'infinite' : '1'

render:
  <span style="
    background-image:           gradient;
    background-size:            200% auto;
    background-position:        -200% center;       // start fully off-screen left
    background-clip:            text;
    -webkit-text-fill-color:    transparent;        // letters become a 'window' to the gradient
    animation-name:             shiny-sweep;
    animation-duration:         {duration}s;
    animation-delay:            {delay}s;
    animation-direction:        animDirection;      // normal | reverse
    animation-iteration-count:  iterationCount;     // infinite | 1
    animation-fill-mode:        forwards;
  ">{text}</span>

keyframes shiny-sweep:
  from: background-position = -200% center  (band fully off-screen left)
  to:   background-position =  200% center  (band fully off-screen right)

prefers-reduced-motion:
  animation: none
  background-image: none           // remove gradient so transparent text isn't invisible
  fill colour: base                // settle on the muted resting colour

The Core Concept: 200%-Wide Gradient + background-clip: text

The trick has two ingredients.

One β€” background-clip: text (with -webkit-text-fill-color: transparent) makes the gradient render only inside the letter shapes. The whitespace between letters stays transparent; the rectangular box that owns the gradient is invisible.

Two β€” the gradient is sized to twice the box width (background-size: 200% auto) and positioned off-screen to the left (background-position: -200% center). Animating background-position from -200% to +200% slides the gradient four element-widths in total, which is enough that a single bright stripe enters from one side, crosses the letters, and exits the other.

  background-size: 200%
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  base ──────── shine ──────── base            β”‚   ← gradient image
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β–²
                     β”‚ background-position slides this strip
                     β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
              β”‚  S H I N Y  β”‚   ← element box (clip window)
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  At -200%:  shine band sits left of the box β†’ letters look base-coloured
  At    0%:  shine band centred β†’ middle letters peak bright
  At +200%:  shine band sits right of the box β†’ letters back to base

Because the gradient stops are base 0% β†’ shine 50% β†’ base 100%, every letter sees the brightest pixel exactly once per sweep, then fades back to base.

CSS Animation Strategy

A single @keyframes block animates background-position from -200% to +200%. Direction is controlled by animation-direction: reverse for 'rl' rather than a second keyframe block β€” both because it is shorter, and because it sidesteps a Safari/older-WebKit bug where CSS custom properties inside @keyframes sometimes fail to resolve.

linear easing keeps the highlight at constant velocity (any other curve would pool the brightness at one edge of the letters). The animation is GPU-friendly β€” background-position triggers only a paint, not a layout, and modern engines composite it cheaply.

For reduced-motion users the @media (prefers-reduced-motion: reduce) block does three things: stops the animation, removes the gradient image (so transparent fill doesn't leave invisible letters), and sets the colour back to --shiny-base. The result is the same word, statically rendered in the muted colour.

Performance

  • Zero JS work after mount. No requestAnimationFrame, no setInterval, no event listeners.
  • One paint per frame on the element's bounding box; modern engines composite paints on the GPU.
  • Safe to drop dozens onto one page β€” a marquee of CTAs costs the same as one because they all share the same composited pipeline.
  • Pure-export helpers (buildShinyGradient, getAnimDirection, getIterationCount) live in the module-script block so unit tests can assert each branch in two lines without rendering.

State Flow Diagram

  [mounted] ── delay elapsed ──▢ [sweeping] ─┐
                                              β”‚ keyframe completes
                                              β–Ό
                                   loop ?  ─── infinite cycle
                                       β”‚
                                       └── one-shot: settle on
                                           background-position: 200%

  prefers-reduced-motion: reduce
        β”‚
        β–Ό
   [static]  no animation, no gradient, base colour fill

Props Reference

Prop Type Default Description
text string required The text to render.
baseColor string '#94a3b8' Resting / muted letter colour.
shineColor string '#ffffff' Bright peak colour at the centre of the band.
duration number 3 Seconds for one full sweep.
direction 'lr' | 'rl' 'lr' Which way the shine moves across the letters.
loop boolean true Repeat indefinitely (true) or sweep once (false).
delay number 0 Seconds to wait before the first sweep starts.
class string '' Extra classes on the wrapper span.

Edge Cases

Situation Behaviour
Empty text prop Renders an empty inline-block span; no animation flicker.
loop = false Plays one sweep, settles at background-position: 200% center (band off-screen right) β€” letters rest at base colour.
User has prefers-reduced-motion: reduce Animation cancelled, gradient removed, letters render in baseColor.
Browser without background-clip: text support Falls back to the literal color: var(--shiny-base) declared above the transparent fill β€” letters appear in the base colour with no shine.
duration set to 0 The keyframe completes instantly; with loop = true you get a still image at the end position.
Letter colour overridden by inherited color The -webkit-text-fill-color: transparent declaration wins over inherited colour β€” the gradient remains visible.

Dependencies

  • Svelte 5.x β€” $derived runes and module-script exports are core to the implementation.
  • Zero external dependencies β€” pure CSS keyframe animation, no animation libraries.

File Structure

src/lib/components/ShinyText.svelte   # implementation
src/lib/components/ShinyText.md       # this file (rendered inside ComponentPageShell)
src/lib/components/ShinyText.test.ts  # vitest unit tests for the helper functions
src/routes/shinytext/+page.svelte     # demo page

API

04
PropTypeDefaultDescription
textstringrequiredText to render with the shine sweep.
baseColorstring"#94a3b8"Resting colour of the text.
shineColorstring"#ffffff"Colour of the highlight band.
durationnumber3Sweep duration in seconds.
direction"lr" | "rl""lr"Sweep direction.
loopbooleantrueLoop continuously or run once.
delaynumber0Delay before the first sweep, seconds.