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

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

Usage
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

PropTypeDefaultDescription
childrenReactNodeHeading 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.
animatebooleantrueSet `false` to skip the entrance animation and render straight into the resting state.
delaynumber0Seconds to delay the entrance animation's start — use it to stagger a second heading's reveal after the first.
classNamestring""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.