# Gradient heading

Display heading with a slow moving gradient sweep through your brand color, with optional highlighted words.

- Type: component
- Category: Text
- Tier: Free
- Dependencies: motion

## Install

```bash
npx shadcn@latest add https://facadeui.com/r/gradient-heading.json
```

## Usage

```tsx
import { GradientHeading } from "@/components/facade/gradient-heading";

export default function Page() {
  return <GradientHeading />;
}
```

## Source

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export interface GradientHeadingProps {
  /** Heading copy. */
  children?: string;
  /** Element to render. Defaults to "h1". */
  as?: "h1" | "h2" | "h3";
  /**
   * Words (or phrases) that get the vivid brand gradient. When omitted the
   * whole heading sweeps through brand and foreground.
   */
  highlight?: string[];
  /** Seconds for one full sweep. Defaults to 9. */
  duration?: number;
  className?: string;
  /** Class applied to highlighted segments. */
  highlightClassName?: string;
}

const vivid =
  "linear-gradient(100deg, var(--foreground) 0%, var(--brand) 22%, color-mix(in oklch, var(--brand) 55%, var(--foreground)) 40%, var(--foreground) 55%, var(--brand) 78%, var(--foreground) 100%)";

const subtle =
  "linear-gradient(100deg, var(--foreground) 0%, color-mix(in oklch, var(--foreground) 62%, transparent) 25%, var(--foreground) 50%, color-mix(in oklch, var(--foreground) 62%, transparent) 75%, var(--foreground) 100%)";

function escapeRegExp(s: string) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function Sweep({
  gradient,
  duration,
  reduce,
  className,
  children,
}: {
  gradient: string;
  duration: number;
  reduce: boolean;
  className?: string;
  children: React.ReactNode;
}) {
  return (
    <motion.span
      className={cn("bg-clip-text text-transparent [-webkit-background-clip:text] [box-decoration-break:clone]", className)}
      style={{ backgroundImage: gradient, backgroundSize: "200% 100%", backgroundPosition: "0% 50%" }}
      animate={reduce ? undefined : { backgroundPosition: ["0% 50%", "100% 50%"] }}
      transition={{ duration, ease: "easeInOut", repeat: Infinity, repeatType: "mirror" }}
    >
      {children}
    </motion.span>
  );
}

export function GradientHeading({
  children = "Websites that make the phone ring.",
  as = "h1",
  highlight,
  duration = 9,
  className,
  highlightClassName,
}: GradientHeadingProps) {
  const reduce = useReducedMotion() ?? false;
  const Tag = as;

  const parts = React.useMemo(() => {
    if (!highlight || highlight.length === 0) return null;
    const re = new RegExp(`(${highlight.map(escapeRegExp).join("|")})`, "g");
    return children.split(re).filter((p) => p.length > 0);
  }, [children, highlight]);

  return (
    <Tag
      className={cn(
        "font-display text-5xl font-semibold leading-[1.02] tracking-[-0.035em] text-balance sm:text-6xl md:text-7xl",
        className,
      )}
    >
      {parts ? (
        parts.map((part, i) =>
          highlight?.includes(part) ? (
            <Sweep key={i} gradient={vivid} duration={duration} reduce={reduce} className={highlightClassName}>
              {part}
            </Sweep>
          ) : (
            <Sweep key={i} gradient={subtle} duration={duration * 1.4} reduce={reduce}>
              {part}
            </Sweep>
          ),
        )
      ) : (
        <Sweep gradient={vivid} duration={duration} reduce={reduce}>
          {children}
        </Sweep>
      )}
    </Tag>
  );
}

export default GradientHeading;
```
