Layout
Gradient Heading
A large heading treatment in two variants: `matte` (solid `--color-on-surface` with a soft layered text-shadow) and `silver` (the same color gradient-clipped into the text itself, `background-clip: text`, plus a matching layered `drop-shadow` filter since a clipped-text element can't use `text-shadow`). Reads `--color-on-surface`, so it retints automatically under the `.light` theme class — no separate light/dark variant needed. Plays a one-shot entrance on mount by default — `matte` fades/rises up out of a blur, `silver` wipes in via an animated `clip-path` — matching the two-line tagline reveal on the homepage. `delay` lets a second heading's reveal overlap the first's, for that same cascading two-line effect.
Preview
Tell me about the project,
we'll design it.
Code
"use client";
import { motion, useReducedMotion } from "framer-motion";
import type { CSSProperties, ReactNode } from "react";
const MATTE_STYLE: CSSProperties = {
color: "var(--color-on-surface)",
textShadow:
"0 10px 30px color-mix(in srgb, var(--color-on-surface) 20%, transparent), 0 2px 4px color-mix(in srgb, var(--color-on-surface) 10%, transparent)",
};
const SILVER_STYLE: CSSProperties = {
background: "linear-gradient(180deg, var(--color-on-surface) 0%, color-mix(in srgb, var(--color-on-surface) 40%, transparent) 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
filter:
"drop-shadow(0px 10px 20px color-mix(in srgb, var(--color-on-surface) 15%, transparent)) drop-shadow(0px 2px 4px color-mix(in srgb, var(--color-on-surface) 10%, transparent))",
};
const MATTE_REVEAL = {
initial: { opacity: 0, y: 24, filter: "blur(12px)" },
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
transition: { duration: 1.1, ease: [0.16, 1, 0.3, 1] as const },
};
const SILVER_REVEAL = {
initial: { clipPath: "inset(0 100% 0 0)" },
animate: { clipPath: "inset(0 0% 0 0)" },
transition: { duration: 1.1, ease: [0.77, 0, 0.18, 1] as const },
};
const TAGS = ["h1", "h2", "h3"] as const;
export function GradientHeading({
as = "h1",
variant = "matte",
animate = true,
delay = 0,
children,
className = "",
}: {
as?: (typeof TAGS)[number];
variant?: "matte" | "silver";
animate?: boolean;
delay?: number;
children: ReactNode;
className?: string;
}) {
const reduced = useReducedMotion();
const MotionTag = motion[as];
const reveal = variant === "silver" ? SILVER_REVEAL : MATTE_REVEAL;
const play = animate && !reduced;
return (
<MotionTag
className={className}
style={variant === "silver" ? SILVER_STYLE : MATTE_STYLE}
initial={play ? reveal.initial : false}
animate={play ? reveal.animate : undefined}
transition={{ ...reveal.transition, delay }}
>
{children}
</MotionTag>
);
}
Installation
import { GradientHeading } from "@/components/ui/GradientHeading";
<GradientHeading as="h1">Tell me about the project,</GradientHeading>
<GradientHeading as="h1" variant="silver" delay={0.6}>we'll design it.</GradientHeading>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | Heading text content. |
| as | "h1" | "h2" | "h3" | "h1" | Which heading element to render. |
| variant | "matte" | "silver" | "matte" | `matte` for a solid color with soft shadow, `silver` for gradient-clipped text with a layered drop-shadow. |
| animate | boolean | true | Set `false` to skip the entrance animation and render straight into the resting state. |
| delay | number | 0 | Seconds to delay the entrance animation's start — use it to stagger a second heading's reveal after the first. |
| className | string | "" | Appended to the heading's classes — this is where font-size/weight/tracking belong, since the component itself only sets the color/shadow treatment. |
Accessibility
Renders as a real heading element (`h1`/`h2`/`h3` via the `as` prop) with its actual text content — the gradient/shadow treatment is purely visual, nothing is hidden from or unavailable to assistive tech. Respects `prefers-reduced-motion`: renders straight into its resting state, no reveal animation.