Skip to content

Components / Text

Gradient Text

A gradient fill drifting slowly through the text with no seam at the loop point. Only the background position moves. Pure CSS; works on any inline or block text.

Live demo

Gradient that never seams.

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

0 B

CSS 4.5 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

Chrome 120 · Firefox 49 · Safari 14; needs bg-clip-text.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
import GradientText from '@moonarc/core/GradientText';
---
<h1><GradientText>Motion for Astro.</GradientText></h1>
<GradientText colors={['#f59e0b', '#ec4899']} angle={45} duration={4}>two colours</GradientText>
Copy it into your project instead (shadcn registry)

Owns the file, no dependency. The registry item also installs the base tokens.

terminal
npx shadcn@latest add https://moonarc.dev/r/gradient-text.json

The CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.

Source

The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.

GradientText.astro
---
/**
 * GradientText — a gradient fill that drifts through the text, zero JS.
 *
 * The gradient is three times wider than the text and only its position
 * moves: no layout, though background-position is not composited, so each
 * frame repaints the text's own box. The last stop repeats the first so the
 * loop has no seam.
 */
import type { HTMLAttributes, HTMLTag } from 'astro/types';

interface Props extends HTMLAttributes<'span'> {
  /** Element to render. */
  as?: HTMLTag;
  /** Colours, 2–5, any CSS colour. */
  colors?: string[];
  /** Seconds per full drift. */
  duration?: number;
  /** Gradient angle in degrees. */
  angle?: number;
}

const { as: Tag = 'span', colors = ['oklch(70% 0.19 300)', 'oklch(78% 0.15 200)', 'oklch(75% 0.17 350)'], duration = 6, angle = 90, class: className, style, ...rest } = Astro.props;
const stops = [...colors.slice(0, 5), colors[0]].join(', ');
// the caller's style joins the component's instead of replacing it: a style prop used to drop the stops, and the text,
// transparent over no gradient, disappeared
const vars = [`--ma-gt-stops:${stops}`, `--ma-gt-duration:${duration}s`, `--ma-gt-angle:${angle}deg`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<Tag class:list={['ma-gradient-text', className]} style={vars} {...rest}><slot /></Tag>

<style is:global>
  @layer components {
    :where(.ma-gradient-text) {
      background-image: linear-gradient(var(--ma-gt-angle, 90deg), var(--ma-gt-stops));
      background-size: 300% 100%;
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      animation: ma-gradient-text var(--ma-gt-duration, 6s) linear infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-gradient-text) {
        animation: none;
      }
    }
    /* Print drops backgrounds by default, and with them the only paint the transparent text had: it prints in the
       colour around it instead. */
    @media print {
      :where(.ma-gradient-text) {
        color: inherit;
        background: none;
        animation: none;
      }
    }
  }

  @keyframes ma-gradient-text {
    to {
      background-position: -300% 0;
    }
  }
</style>

Props

PropTypeDefaultDescription
asHTMLTag'span'Element to render.
colorsstring[]violet, cyan, rose (oklch)2–5 colours. The first is repeated at the end so the loop has no visible seam.
durationnumber6Seconds per full drift. Under 3 flickers.
anglenumber90Gradient angle in degrees.

Reduced motion

The gradient stays put.

With ClientRouter

CSS-only; nothing to rebind.

Why it is built this way

Replaces: GradientText (React Bits) · AnimatedGradientText (Magic UI). See the migration table.

Ambient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps.

every browseralways

A highlight sweeps across the text every few seconds.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown