Swish UI
All componentsText Free

Shiny text

A muted label with a soft band of light sweeping across it every few seconds.

It imports two small hooks from @/lib/motion and the colour tokens; the zip includes both.

Demo.tsx

"use client";
import { useRef } from "react";
import { useInView } from "@/lib/motion";
import styles from "./demo.module.css";

export default function Demo() {
  const root = useRef<HTMLDivElement>(null);
  const inView = useInView(root);

  return (
    <div ref={root} className={`${styles.root} ${inView ? "" : styles.paused}`}>
      <p className={styles.badge}>
        <span className={styles.dot} aria-hidden="true" />
        <span className={styles.shine}>New: instant payouts for hosts</span>
      </p>
      <h2 className={styles.title}>Get paid the day your guests check in</h2>
      <p className={styles.status}>
        <span className={styles.shine}>Checking availability across 318 villas</span>
      </p>
    </div>
  );
}

demo.module.css

.root { container-type: size; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 20px; text-align: center; }
.badge { display: inline-flex; align-items: center; gap: 8px; padding: 7px 14px; border-radius: 999px; background: var(--white); border: 1px solid var(--line); font-size: 14px; font-weight: 600; }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.title { max-width: 14em; font-size: clamp(22px, min(6cqi, 10cqh), 44px); line-height: 1.15; letter-spacing: -0.03em; font-weight: 650; text-wrap: balance; }
.status { font-size: 15px; font-weight: 500; }
.shine {
  color: transparent;
  background: linear-gradient(100deg, var(--muted) 42%, color-mix(in srgb, var(--muted) 12%, var(--white)) 50%, var(--muted) 58%) 100% 0 / 300% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  animation: sweep 3.2s ease-in-out infinite;
}
.status .shine { animation-delay: 0.8s; }
.paused .shine { animation-play-state: paused; }
@keyframes sweep { 70%, 100% { background-position: 0% 0; } }
@media (prefers-reduced-motion: reduce) {
  .shine { animation: none; background: none; color: var(--muted); }
}

Use it for

  • A 'new' badge above a hero headline
  • Loading and status lines, where motion says work is happening

Avoid

  • Primary content and headlines, the shine lowers contrast while it passes
  • More than two shiny elements on screen

How it works

The text colour is transparent and a three stop linear-gradient (muted, near white, muted) is painted through it with background-clip: text. The gradient is 300 percent wide, so moving background-position from 100 percent to 0 carries the bright band across the letters. The sweep takes 70 percent of each cycle and the rest is a pause. A class sets animation-play-state: paused while the demo is off screen.

Accessibility

It is plain text, so screen readers read it normally. Reduced motion removes the gradient and shows solid muted text. The muted base colour keeps enough contrast while the band is away.

Found on