Text
Shiny Text
A restrained inline shimmer — one narrow light band loops across muted text, for New badges and quiet links.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/shiny-text.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "ShinyText" component (pure CSS, no
hooks, no animation library — it stays a server component).
Contract
- Export a forwardRef element extending React.HTMLAttributes<HTMLElement>.
- Props: as (rendered tag: span | a | strong | p | div | h2..h4, default
"span"), children, speed (seconds per sweep, default 5), intensity =
"subtle" | "medium" (default "subtle"), disabled (default false).
- No size, weight or spacing of its own: it inherits the surrounding
typography, so the same component works inside a 12px badge and a 24px
heading.
Behavior
- The shine is a narrow bright band inside a mostly-flat gradient that is
clipped to the glyphs and slid across them on an infinite loop. Base color
is var(--muted-foreground), the peak is var(--foreground) (medium) or a
color-mix of the two (subtle) — both tokens flip with the color scheme, so
the effect is readable in light and dark without a single override.
- Keep the gradient strictly horizontal ("to right"). The background tiles
while it scrolls; a slanted gradient would show a visible seam at the tile
edge, a horizontal one has uniform edges and loops invisibly.
- Loop by animating background-position from 200% to 0% with background-size
200% auto: one full tile of travel per cycle, band sweeping left to right.
- Put the keyframes, the animation rule and the reduced-motion override in
ONE hoisted <style href precedence="medium"> tag (React 19 dedupes by
href). Write the selector twice (.cls.cls) so the rule outranks any single
utility class the consumer passes — otherwise a text-* class in className
would silently cancel the transparent color the effect depends on.
- Degradation must be complete, not partial: for prefers-reduced-motion the
same stylesheet sets background-image: none and color: inherit, and
disabled={true} simply omits the class. Either way you get ordinary
inherited text — never a half-transparent ghost.
- Feed the gradient and the duration in through CSS custom properties set
inline (--band, --speed), NOT as inline background-image/animation: inline
styles would beat the media-query override and break the fallback.
Rendering & styling
- Semantic tokens only: var(--muted-foreground), var(--foreground), and
color-mix(in oklab, ...) for the softer peak. No hardcoded colors.
- Because the glyphs are painted by the background, currentColor is
transparent inside the element: any decoration must carry its own color
(e.g. underline + decoration-muted-foreground on a link).
- Accessibility: this is a purely decorative paint on real text, so the text
stays in the document, selectable and announced normally — no aria-hidden,
no sr-only duplicate. The rendered tag keeps its semantics (a link is
still a link and takes href / onClick from the consumer).
Customization levers
- Intensity: "subtle" for badges and links (narrow band, low peak),
"medium" for headings; the two are just different stop positions and peak
colors — add a third preset by copying one entry.
- Speed: 4-6s reads as ambient; under 2s it starts to look like a loading
state; over 8s it becomes a slow "breathing" accent.
- Palette: swap the base token (muted-foreground -> primary/60) for a tinted
shimmer, or the peak (foreground -> primary) to shine in brand color.
- Kill switch: disabled is a prop, so a user setting ("reduce effects") or a
density mode can turn the shimmer off app-wide without changing markup.
- Tag: as="a" for links, as="strong" inside a sentence, as="h3" for a
section title — styling is identical, semantics are yours.Concepts
- Ambient, not entrance — there is no start and no end state: the band simply keeps passing, which makes it suitable for elements that live on screen permanently (a badge, a nav link) rather than for an arrival animation.
- Muted base, bright peak — the flat part of the gradient is
muted-foregroundand only the band reachesforeground, so the text reads as secondary while the highlight supplies the motion; both tokens invert with the theme, so contrast holds in dark mode. - Horizontal gradients tile seamlessly — the background repeats as it scrolls, and only a strictly horizontal ramp has uniform tile edges; a diagonal one would flash a seam once per cycle.
- Complete degradation — under
prefers-reduced-motionordisabled, the background and the transparent color are removed together, so the fallback is ordinary text instead of a washed-out ghost. - CSS variables in, overrides out — the gradient and duration arrive as custom properties, which keeps the actual declarations inside the component's stylesheet where the reduced-motion media query can still override them (an inline
background-imagecould not be beaten). - No currentColor inside clipped text — the element's own color is transparent, so underlines, carets and shadows on the same element need an explicit color token.