Browse the library
Components / Text
Gradient heading
FreeDisplay heading with a slow moving gradient sweep through your brand color, with optional highlighted words.
View as Markdownmotion
components/facade/gradient-heading.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;app/page.tsx
import { ArrowRight } from "lucide-react";
import { GradientHeading } from "@/components/facade/gradient-heading";
export default function Demo() {
return (
<div className="w-full max-w-3xl py-6 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground">
<span className="rounded-full bg-brand px-1.5 py-px text-[10px] font-semibold uppercase tracking-wide text-brand-foreground">
New
</span>
Now serving Miami and Fort Lauderdale
</span>
<GradientHeading highlight={["calls you back"]} className="mx-auto mt-6 max-w-2xl">
The roofing crew that actually calls you back.
</GradientHeading>
<p className="mx-auto mt-6 max-w-md text-base leading-relaxed text-muted-foreground sm:text-lg">
Same day quotes, fixed pricing and a 10 year workmanship warranty on every job.
</p>
<a
href="#"
className="group mt-8 inline-flex h-11 items-center gap-2 rounded-full bg-primary px-6 text-sm font-medium text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Get a same day quote
<ArrowRight className="size-4 transition-transform duration-300 group-hover:translate-x-0.5" />
</a>
</div>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/gradient-heading.jsonUsage
import { GradientHeading } from "@/components/facade/gradient-heading";
export default function Page() {
return <GradientHeading />;
}Props
GradientHeadingProps is exported, so you can extend or wrap it. Every prop has a default; <GradientHeading /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| children | string | Heading copy. |
| as | "h1" | "h2" | "h3" | Element to render. Defaults to "h1". |
| highlight | string[] | Words (or phrases) that get the vivid brand gradient. When omitted the whole heading sweeps through brand and foreground. |
| duration | number | Seconds for one full sweep. Defaults to 9. |
| className | string | |
| highlightClassName | string | Class applied to highlighted segments. |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade gradient heading to this page and adapt the copy to our business.