Shiny Text — Free HTML CSS Animated Shimmer Text Snippet

Shiny Text · Animations · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

background-clip shine
A gradient painted into the letters.
Moving highlight band
background-position sweeps the sheen.
Seamless loop
Matching gray ends hide the restart.
No pseudo or overlay
Pure color and background, no extra DOM.
Drop-in class
Works on badges, headings, and links.
Reduced-motion safe
Falls back to solid color when requested.
Runtime speed API
Retune the sweep on the fly.
Multi-line friendly
Follows the exact glyph shapes.

About this UI Snippet

Shiny Text — A Sheen That Sweeps Across Muted Text

Screenshot of the Shiny Text snippet rendered live

Shiny text is the subtle shimmer seen on "Introducing" badges and premium headlines: the text sits in a muted gray, and a soft band of light glides across it repeatedly, like a sheen catching the letters. This snippet builds it with pure CSS — no pseudo-elements, no JavaScript for the shine — plus a small script for accessibility and runtime tuning.

A gradient clipped to the text

The shine is a single trick: the text color is set to transparent and filled with a linear-gradient via background-clip: text, so the gradient shows through the letter shapes. The gradient is mostly the muted base gray with a bright white band in the middle, and it is sized at 220% width so there is room to slide. Because background-clip: text paints the gradient into the glyphs, the bright band only appears where there are letters — exactly the sheen effect.

Animating the sweep

The stShine keyframe animates background-position from one side to the other, dragging the bright band across the text on a 3-second linear loop. Since the rest of the gradient is the same muted gray on both ends, the band slides in from one edge and out the other with no visible seam — it reads as a continuous, repeating glint. This is far cheaper than a masked overlay or a JavaScript animation, because only background-position changes and the browser composites it efficiently.

Reusable on any text

The effect is a single class, .st-shiny, applied to whatever you want to shimmer — here both a pill badge and a large headline. Because it only touches color and background, you can drop it onto links, buttons, headings, or inline spans without changing their layout. The demo badge also nudges its arrow on hover, a common pattern for "new feature" announcement chips.

Respecting reduced motion

Continuous shimmer can be distracting, so the script checks prefers-reduced-motion via matchMedia and, when set, removes the animation and restores a solid readable color on every shiny element. This keeps the text legible and calm for users who have asked for less motion — the accessible default for any looping decorative effect.

Runtime control

A tiny window.shinyText.speed(seconds) helper lets you retune the sweep speed at runtime by updating the animation duration on all shiny elements. It is optional — the effect works without any JavaScript — but it is handy if you want the shimmer to speed up on interaction or differ between elements.

Why background-clip over a mask

Some shimmer effects use a moving highlight with mix-blend-mode or a clipped overlay element. Clipping a gradient to the text is simpler and more robust: it needs no extra DOM, automatically follows the exact letter shapes including descenders and serifs, and works on multi-line text. The only caveat is that the text must be transparent, so provide a solid fallback color (as the reduced-motion branch does) for contexts where the clip is unsupported.

Customizing it

Adjust the gradient stops to widen or sharpen the bright band, change the 220% size and the keyframe range to alter how far it travels, retime the loop, or recolor the base and highlight (a gold base with a white band reads as metallic). Pair it with a retro grid hero or a border beam card for a polished announcement.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA badge and headline render in muted gray.
  2. 2
    Watch the shineA band of light sweeps across the text on a loop.
  3. 3
    Hover the badgeThe arrow nudges right like a feature chip.
  4. 4
    Apply it anywhereAdd the st-shiny class to any text.
  5. 5
    Check reduced motionEnable the OS setting and the shimmer turns off.
  6. 6
    Tune the speedCall window.shinyText.speed(seconds).

Real-world uses

Common Use Cases

Announcement badges
Introduce a release near a shimmer button.
Premium headlines
Shine a title over a retro grid hero.
Pricing highlights
Mark a popular tier on a pricing card.
Beta and new chips
Pair with a sticky promo bar.
Loading labels
A calmer cousin of an ai thinking loader.
Text effect demos
A reference for clipped-gradient shimmer.

Got questions?

Frequently Asked Questions

The text color is set to transparent and filled with a linear-gradient via background-clip: text, so the gradient only shows through the letter shapes. The gradient is mostly the muted base gray with a bright band in the middle, so the highlight appears only where there are glyphs — that is the sheen.

The stShine keyframe animates background-position across an oversized gradient. Because both ends of the gradient are the same muted gray, the bright band slides in from one edge and out the other with no visible jump, reading as a continuous repeating glint. Only background-position changes, so it composites cheaply.

Clipping a gradient to the text needs no extra DOM, automatically follows the exact letter shapes including descenders, and works across multiple lines. A masked highlight overlay is more elements and harder to align. The one caveat is the text must be transparent, so provide a solid fallback color where the clip is unsupported.

Yes. The script checks prefers-reduced-motion with matchMedia and, when set, removes the animation and restores a solid readable color on every shiny element. Honoring that media query is the accessible default for any looping decorative animation, and it keeps the text legible for users who want less motion.

Make a small ShinyText component that applies the class to its children, and read prefers-reduced-motion in a mount effect to disable the animation when requested. Expose a speed prop that sets animation-duration via inline style. The shine itself is pure CSS. In Tailwind, define the shimmer keyframe in the config and use bg-clip-text with text-transparent and an arbitrary gradient.

Shiny Text — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Shiny Text</title>
  <style>
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#07070f;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;text-align:center;padding:24px}
    
    .st-badge{display:inline-flex;align-items:center;gap:8px;text-decoration:none;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:8px 16px;font-size:13.5px;font-weight:600;transition:background .2s}
    .st-badge:hover{background:rgba(255,255,255,.1)}
    .st-arrow{transition:transform .2s;color:#a5b4fc}
    .st-badge:hover .st-arrow{transform:translateX(3px)}
    
    /* The shine: a light band travels across muted text via a moving
       background-clip gradient. No pseudo-elements, no JS. */
    .st-shiny{
      color:transparent;
      background:linear-gradient(110deg,#6b6b82 0%,#6b6b82 40%,#ffffff 50%,#6b6b82 60%,#6b6b82 100%);
      background-size:220% 100%;
      -webkit-background-clip:text;background-clip:text;
      animation:stShine 3s linear infinite;
    }
    @keyframes stShine{0%{background-position:120% 0}100%{background-position:-120% 0}}
    
    .st-big{font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em}
    .st-sub{color:#7a7a92;font-size:15px;max-width:440px;line-height:1.55}
  </style>
</head>
<body>
  <div class="st-stage">
    <a href="#" class="st-badge"><span class="st-shiny">✨ Introducing v2.0</span><span class="st-arrow">→</span></a>
    <h1 class="st-shiny st-big">Shimmer that sweeps</h1>
    <p class="st-sub">A muted sheen glides across the text on a loop — no JS required for the shine.</p>
  </div>
  <script>
    // The shimmer is pure CSS. JS only respects reduced-motion and lets you tune
    // the speed/intensity at runtime via data attributes.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      document.querySelectorAll('.st-shiny').forEach(function (el) {
        el.style.animation = 'none';
        el.style.color = '#cfcfe0';
      });
    }
    
    window.shinyText = {
      speed: function (seconds) {
        document.querySelectorAll('.st-shiny').forEach(function (el) {
          el.style.animationDuration = seconds + 's';
        });
      }
    };
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Shiny Text</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    @keyframes stShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

    * {
      box-sizing:border-box;margin:0;padding:0
    }

    body {
      font-family:system-ui,-apple-system,sans-serif;background:#07070f;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;text-align:center;padding:24px
    }

    .st-badge:hover .st-arrow {
      transform:translateX(3px)
    }
  </style>
</head>
<body>
  <div class="st-stage">
    <a href="#" class="st-badge inline-flex items-center gap-2 no-underline bg-[rgba(255,255,255,.05)] border border-[rgba(255,255,255,.12)] rounded-[999px] py-2 px-4 text-[13.5px] font-semibold [transition:background_.2s] hover:bg-[rgba(255,255,255,.1)]"><span class="st-shiny text-transparent [background:linear-gradient(110deg,#6b6b82_0%,#6b6b82_40%,#ffffff_50%,#6b6b82_60%,#6b6b82_100%)] bg-[size:220%_100%] [-webkit-background-clip:text] bg-clip-text [animation:stShine_3s_linear_infinite]">✨ Introducing v2.0</span><span class="st-arrow [transition:transform_.2s] text-[#a5b4fc]">→</span></a>
    <h1 class="st-shiny text-transparent [background:linear-gradient(110deg,#6b6b82_0%,#6b6b82_40%,#ffffff_50%,#6b6b82_60%,#6b6b82_100%)] bg-[size:220%_100%] [-webkit-background-clip:text] bg-clip-text [animation:stShine_3s_linear_infinite] text-[clamp(34px,8vw,72px)] font-black tracking-[-.03em]">Shimmer that sweeps</h1>
    <p class="text-[#7a7a92] text-[15px] max-w-[440px] leading-[1.55]">A muted sheen glides across the text on a loop — no JS required for the shine.</p>
  </div>
  <script>
    // The shimmer is pure CSS. JS only respects reduced-motion and lets you tune
    // the speed/intensity at runtime via data attributes.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      document.querySelectorAll('.st-shiny').forEach(function (el) {
        el.style.animation = 'none';
        el.style.color = '#cfcfe0';
      });
    }
    
    window.shinyText = {
      speed: function (seconds) {
        document.querySelectorAll('.st-shiny').forEach(function (el) {
          el.style.animationDuration = seconds + 's';
        });
      }
    };
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to ShinyText.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#07070f;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;text-align:center;padding:24px}

.st-badge{display:inline-flex;align-items:center;gap:8px;text-decoration:none;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:8px 16px;font-size:13.5px;font-weight:600;transition:background .2s}
.st-badge:hover{background:rgba(255,255,255,.1)}
.st-arrow{transition:transform .2s;color:#a5b4fc}
.st-badge:hover .st-arrow{transform:translateX(3px)}

/* The shine: a light band travels across muted text via a moving
   background-clip gradient. No pseudo-elements, no JS. */
.st-shiny{
  color:transparent;
  background:linear-gradient(110deg,#6b6b82 0%,#6b6b82 40%,#ffffff 50%,#6b6b82 60%,#6b6b82 100%);
  background-size:220% 100%;
  -webkit-background-clip:text;background-clip:text;
  animation:stShine 3s linear infinite;
}
@keyframes stShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

.st-big{font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em}
.st-sub{color:#7a7a92;font-size:15px;max-width:440px;line-height:1.55}
`;

export default function ShinyText() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    // The shimmer is pure CSS. JS only respects reduced-motion and lets you tune
    // the speed/intensity at runtime via data attributes.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      document.querySelectorAll('.st-shiny').forEach(function (el) {
        el.style.animation = 'none';
        el.style.color = '#cfcfe0';
      });
    }
    
    window.shinyText = {
      speed: function (seconds) {
        document.querySelectorAll('.st-shiny').forEach(function (el) {
          el.style.animationDuration = seconds + 's';
        });
      }
    };
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="st-stage">
        <a href="#" className="st-badge"><span className="st-shiny">✨ Introducing v2.0</span><span className="st-arrow">→</span></a>
        <h1 className="st-shiny st-big">Shimmer that sweeps</h1>
        <p className="st-sub">A muted sheen glides across the text on a loop — no JS required for the shine.</p>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function ShinyText() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    // The shimmer is pure CSS. JS only respects reduced-motion and lets you tune
    // the speed/intensity at runtime via data attributes.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      document.querySelectorAll('.st-shiny').forEach(function (el) {
        el.style.animation = 'none';
        el.style.color = '#cfcfe0';
      });
    }
    
    window.shinyText = {
      speed: function (seconds) {
        document.querySelectorAll('.st-shiny').forEach(function (el) {
          el.style.animationDuration = seconds + 's';
        });
      }
    };
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };
  }, []);

  return (
    <>
      <style>{`
@keyframes stShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

* {
  box-sizing:border-box;margin:0;padding:0
}

body {
  font-family:system-ui,-apple-system,sans-serif;background:#07070f;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;text-align:center;padding:24px
}

.st-badge:hover .st-arrow {
  transform:translateX(3px)
}
      `}</style>
      <div className="st-stage">
        <a href="#" className="st-badge inline-flex items-center gap-2 no-underline bg-[rgba(255,255,255,.05)] border border-[rgba(255,255,255,.12)] rounded-[999px] py-2 px-4 text-[13.5px] font-semibold [transition:background_.2s] hover:bg-[rgba(255,255,255,.1)]"><span className="st-shiny text-transparent [background:linear-gradient(110deg,#6b6b82_0%,#6b6b82_40%,#ffffff_50%,#6b6b82_60%,#6b6b82_100%)] bg-[size:220%_100%] [-webkit-background-clip:text] bg-clip-text [animation:stShine_3s_linear_infinite]">✨ Introducing v2.0</span><span className="st-arrow [transition:transform_.2s] text-[#a5b4fc]">→</span></a>
        <h1 className="st-shiny text-transparent [background:linear-gradient(110deg,#6b6b82_0%,#6b6b82_40%,#ffffff_50%,#6b6b82_60%,#6b6b82_100%)] bg-[size:220%_100%] [-webkit-background-clip:text] bg-clip-text [animation:stShine_3s_linear_infinite] text-[clamp(34px,8vw,72px)] font-black tracking-[-.03em]">Shimmer that sweeps</h1>
        <p className="text-[#7a7a92] text-[15px] max-w-[440px] leading-[1.55]">A muted sheen glides across the text on a loop — no JS required for the shine.</p>
      </div>
    </>
  );
}
Vue
<template>
  <div class="st-stage">
    <a href="#" class="st-badge"><span class="st-shiny">✨ Introducing v2.0</span><span class="st-arrow">→</span></a>
    <h1 class="st-shiny st-big">Shimmer that sweeps</h1>
    <p class="st-sub">A muted sheen glides across the text on a loop — no JS required for the shine.</p>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

// The shimmer is pure CSS. JS only respects reduced-motion and lets you tune
// the speed/intensity at runtime via data attributes.
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

onMounted(() => {
  if (reduce) {
    document.querySelectorAll('.st-shiny').forEach(function (el) {
      el.style.animation = 'none';
      el.style.color = '#cfcfe0';
    });
  }
  
  window.shinyText = {
    speed: function (seconds) {
      document.querySelectorAll('.st-shiny').forEach(function (el) {
        el.style.animationDuration = seconds + 's';
      });
    }
  };
});
</script>

<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#07070f;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;text-align:center;padding:24px}

.st-badge{display:inline-flex;align-items:center;gap:8px;text-decoration:none;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:8px 16px;font-size:13.5px;font-weight:600;transition:background .2s}
.st-badge:hover{background:rgba(255,255,255,.1)}
.st-arrow{transition:transform .2s;color:#a5b4fc}
.st-badge:hover .st-arrow{transform:translateX(3px)}

/* The shine: a light band travels across muted text via a moving
   background-clip gradient. No pseudo-elements, no JS. */
.st-shiny{
  color:transparent;
  background:linear-gradient(110deg,#6b6b82 0%,#6b6b82 40%,#ffffff 50%,#6b6b82 60%,#6b6b82 100%);
  background-size:220% 100%;
  -webkit-background-clip:text;background-clip:text;
  animation:stShine 3s linear infinite;
}
@keyframes stShine{0%{background-position:120% 0}100%{background-position:-120% 0}}

.st-big{font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em}
.st-sub{color:#7a7a92;font-size:15px;max-width:440px;line-height:1.55}
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-shiny-text',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="st-stage">
      <a href="#" class="st-badge"><span class="st-shiny">✨ Introducing v2.0</span><span class="st-arrow">→</span></a>
      <h1 class="st-shiny st-big">Shimmer that sweeps</h1>
      <p class="st-sub">A muted sheen glides across the text on a loop — no JS required for the shine.</p>
    </div>
  `,
  styles: [`
    *{box-sizing:border-box;margin:0;padding:0}
    body{font-family:system-ui,-apple-system,sans-serif;background:#07070f;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;text-align:center;padding:24px}
    
    .st-badge{display:inline-flex;align-items:center;gap:8px;text-decoration:none;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);border-radius:999px;padding:8px 16px;font-size:13.5px;font-weight:600;transition:background .2s}
    .st-badge:hover{background:rgba(255,255,255,.1)}
    .st-arrow{transition:transform .2s;color:#a5b4fc}
    .st-badge:hover .st-arrow{transform:translateX(3px)}
    
    /* The shine: a light band travels across muted text via a moving
       background-clip gradient. No pseudo-elements, no JS. */
    .st-shiny{
      color:transparent;
      background:linear-gradient(110deg,#6b6b82 0%,#6b6b82 40%,#ffffff 50%,#6b6b82 60%,#6b6b82 100%);
      background-size:220% 100%;
      -webkit-background-clip:text;background-clip:text;
      animation:stShine 3s linear infinite;
    }
    @keyframes stShine{0%{background-position:120% 0}100%{background-position:-120% 0}}
    
    .st-big{font-size:clamp(34px,8vw,72px);font-weight:900;letter-spacing:-.03em}
    .st-sub{color:#7a7a92;font-size:15px;max-width:440px;line-height:1.55}
  `]
})
export class ShinyTextComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    // The shimmer is pure CSS. JS only respects reduced-motion and lets you tune
    // the speed/intensity at runtime via data attributes.
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduce) {
      document.querySelectorAll('.st-shiny').forEach(function (el) {
        el.style.animation = 'none';
        el.style.color = '#cfcfe0';
      });
    }
    
    window.shinyText = {
      speed: function (seconds) {
        document.querySelectorAll('.st-shiny').forEach(function (el) {
          el.style.animationDuration = seconds + 's';
        });
      }
    };
  }
}