Text Animationsv0.1.0
Gradient Text
A slow gradient wash moves across the letters.
Installation
CLI
npx shadcn@latest add https://hantui-theta.vercel.app/r/gradient-text.jsonManual
Install the dependencies
npm install clsx motion tailwind-mergeAdd the shared helpers
Components use
cn()fromlib/utils.ts. See Installation if you have not set them up yet.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
| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | – | Text to color. |
colors | string[] | ["var(--primary)", "#7C3AED", "#DB2777"] | Gradient stops, left to right. |
duration | number | 8 | Seconds for the wash to travel one full cycle. |
paused | boolean | false | Hold 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.