Gradient Text
TextA gradient fill drifting slowly through the text with no seam at the loop point.
A highlight sweeps across the text every few seconds. The text keeps its own colour; the shine is a gradient clipped to a repeated copy on top, and only its position animates. Pure CSS.
Measured from a real build.
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 4.7 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 120 · Firefox 49 · Safari 14; needs bg-clip-text.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import ShinyText from '@moonarc/core/ShinyText';
---
<h2><ShinyText text="Measured, not estimated." /></h2>
<ShinyText text="Pro" color="oklch(85% 0.17 70)" duration={2} />Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/shiny-text.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* ShinyText — a highlight sweeps across the text on a loop, zero JS.
*
* The text is painted once in its own colour; a pseudo-element repeats it
* with a moving gradient clipped to the glyphs on top. Only background-
* position animates: no layout, but it is not composited either, so each
* frame repaints the text's own box.
*/
import type { HTMLAttributes, HTMLTag } from 'astro/types';
interface Props extends HTMLAttributes<'span'> {
/** The text. */
text: string;
/** Element to render. */
as?: HTMLTag;
/** Seconds per sweep, including the pause. */
duration?: number;
/** Highlight colour. */
color?: string;
/** Width of the highlight as a fraction of the text, 0–1. */
width?: number;
}
const { text, as: Tag = 'span', duration = 3, color = 'var(--ma-shine)', width = 0.25, class: className, style, ...rest } = Astro.props;
const half = Math.max(0.05, Math.min(width, 0.6)) * 50;
// the caller's style joins the component's properties instead of replacing them (duration, colour and width were lost)
const vars = [`--ma-shiny-duration:${duration}s`, `--ma-shiny-color:${color}`, `--ma-shiny-a:${50 - half}%`, `--ma-shiny-b:${50 + half}%`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<Tag class:list={['ma-shiny', className]} data-text={text} style={vars} {...rest}>{text}</Tag>
<style is:global>
@layer components {
:where(.ma-shiny) {
position: relative;
display: inline-block;
}
:where(.ma-shiny)::after {
/* the copy is paint only: the alternative text after the slash is empty, so assistive tech reads the sentence
once (generated content is part of an element's name). The first line is for engines without that syntax. */
content: attr(data-text);
content: attr(data-text) / '';
position: absolute;
inset: 0;
pointer-events: none;
color: transparent;
background: linear-gradient(110deg, transparent var(--ma-shiny-a, 37.5%), var(--ma-shiny-color, var(--ma-shine)) 50%, transparent var(--ma-shiny-b, 62.5%));
background-size: 250% 100%;
background-position: 120% 0;
-webkit-background-clip: text;
background-clip: text;
animation: ma-shiny var(--ma-shiny-duration, 3s) var(--ma-ease-in-out) infinite;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-shiny)::after {
animation: none;
opacity: 0;
}
}
}
@keyframes ma-shiny {
0% {
background-position: 120% 0;
}
60%,
100% {
background-position: -20% 0;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
text | string | none | The text; repeated in a pseudo-element, so it must be a plain string. |
as | HTMLTag | 'span' | Element to render. |
duration | number | 3 | Seconds per cycle; the sweep takes 60% of it and the rest is a pause. |
color | string | var(--ma-shine) | Highlight colour; the token is theme-aware. |
width | number | 0.25 | Highlight width as a fraction of the text, 0.05–0.6. |
The sweep is removed; the text shows in its own colour.
ClientRouterCSS-only; nothing to rebind.
Replaces: ShinyText (React Bits) · AnimatedShinyText (Magic UI). See the migration table.
A gradient fill drifting slowly through the text with no seam at the loop point.
Splits a string into words, characters or lines on the server and reveals them with a capped stagger when they scroll into view.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown