Skip to content

Gradient Text

A slow gradient wash moves across the letters.

Installation

CLI

npx shadcn@latest add https://hantui-theta.vercel.app/r/gradient-text.json

Manual

  1. Install the dependencies

    npm install clsx motion tailwind-merge
  2. Add the shared helpers

    Components use cn() from lib/utils.ts. See Installation if you have not set them up yet.

  3. Copy the source

    components/ui/gradient-text/gradient-text.tsx
    "use client";
    
    import type { ComponentProps, ReactNode } from "react";
    import { motion, useReducedMotion } from "motion/react";
    import { cn } from "@/lib/utils";
    
    export interface GradientTextProps
      extends Omit<ComponentProps<"span">, "children" | "onAnimationStart" | "onDrag" | "onDragStart" | "onDragEnd"> {
      /** Text to color. */
      children: ReactNode;
      /** Gradient stops, left to right. @default ["var(--primary)", "#7C3AED", "#DB2777"] */
      colors?: string[];
      /** Seconds for the wash to travel one full cycle. @default 8 */
      duration?: number;
      /** Hold the gradient still. It also holds still under reduced motion. @default false */
      paused?: boolean;
    }
    
    const DEFAULT_COLORS = ["var(--primary)", "#7C3AED", "#DB2777"];
    
    export function GradientText({ children, colors = DEFAULT_COLORS, duration = 8, paused = false, className, style, ...props }: GradientTextProps) {
      const reduceMotion = useReducedMotion();
      const moving = !paused && !reduceMotion;
      // Two copies of the palette across a 200%-wide background: shifting by half its width loops seamlessly.
      const stops = [...colors, ...colors, colors[0]].join(", ");
    
      return (
        <motion.span
          className={cn(
            "bg-clip-text text-transparent [-webkit-background-clip:text] [box-decoration-break:clone]",
            "selection:text-foreground forced-colors:bg-none forced-colors:text-[CanvasText]",
            className,
          )}
          style={{ ...style, backgroundImage: `linear-gradient(90deg, ${stops})`, backgroundSize: "200% 100%" }}
          initial={{ backgroundPosition: "100% 50%" }}
          // background-position only repaints the glyphs; it never triggers layout.
          animate={moving ? { backgroundPosition: ["100% 50%", "0% 50%"] } : { backgroundPosition: "100% 50%" }}
          transition={moving ? { duration, ease: "linear", repeat: Infinity } : { duration: 0.6 }}
          {...props}
        >
          {children}
        </motion.span>
      );
    }

Usage

import { GradientText } from "@/components/ui/gradient-text/gradient-text";

export function Hero() {
  return (
    <h1 className="text-5xl font-semibold">
      Interfaces that feel <GradientText>alive</GradientText>
    </h1>
  );
}

API reference

PropTypeDefaultDescription
children*ReactNode–Text to color.
colorsstring[]["var(--primary)", "#7C3AED", "#DB2777"]Gradient stops, left to right.
durationnumber8Seconds for the wash to travel one full cycle.
pausedbooleanfalseHold the gradient still. It also holds still under reduced motion.

Also accepts every native <span> attribute, including ref and className.

  • The wash moves by animating background-position, which only repaints the glyphs and never triggers layout. It's the one effect here that can't be done with transforms alone, so keep it to a heading or a phrase.
  • Two copies of the palette across a 200%-wide background make the loop seamless.

Accessibility

  • Pick colors with at least 4.5:1 contrast against the background (3:1 for large headings), since the text is drawn in every stop. The defaults all clear 4.5:1 on white.
  • In forced-colors (high contrast) mode the gradient is dropped and the text uses the system text color.
  • With reduced motion enabled, the gradient holds still.