Skip to content
Dethink Components

ComponentsHeroTextAnimation

HeroTextAnimation

Reveal a headline one word or line at a time.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

Import the component into your page or component file.

Usage
import {
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";

export function Example() {
  return (
    <HeroTextAnimationProvider>
      <HeroTextAnimation text="Build production-ready landing pages faster." />
    </HeroTextAnimationProvider>
  );
}

Each animation is shown in a distinct, production-style hero tuned to where it fits best — SaaS, developer tools, security, editorial, luxury, and more. Expand a source panel when you need the implementation.

For liquid, prismatic, metallic, and mouse-following particle lettering, explore ShaderHeroText.

SaaS product hero — staggered words

The default staggered word reveal in a conversion-focused SaaS layout: navbar, dual CTAs, and an icon-led proof grid.

Hero section recipe

Turn your launch headline into a clear first impression.

Pair the animated headline with plain-language support copy, clear actions, and icon-led proof points so the hero stays usable before and after the motion completes.

  • Readable firstThe final headline stays present in server-rendered HTML.
  • Motion awareReduced-motion visitors get a calm static or opacity-only state.
  • Fast by defaultShort stagger timing keeps the hero clear without delaying action.
  • Easy to scanIcon and text pairs turn hero promises into quick proof points.
Show sourceexamples/hero-text-animation/production-hero.tsx
examples/hero-text-animation/production-hero.tsx
"use client";

import {
  Button,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuItemDescription,
  DropdownMenuItemIcon,
  DropdownMenuItemLabel,
  DropdownMenuLabel,
  DropdownMenuSection,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  HeroTextAnimation,
  HeroTextAnimationProvider,
  RevealButton,
} from "@dethink/components";
import {
  ArrowRight,
  BookOpenText,
  ChevronDown,
  Eye,
  Gauge,
  Layers3,
  LifeBuoy,
  PlayCircle,
  Rocket,
  ShieldCheck,
  Sparkles,
} from "lucide-react";

const navItems = [
  { href: "#examples", label: "Examples" },
  { href: "#installation", label: "Install" },
  { href: "#props", label: "API" },
];

const resourceItems = [
  {
    icon: Layers3,
    label: "Animation patterns",
    description: "Choose a safe motion style for each hero message.",
  },
  {
    icon: ShieldCheck,
    label: "Accessibility model",
    description: "Keep one readable heading while fragments animate visually.",
  },
  {
    icon: LifeBuoy,
    label: "Implementation notes",
    description: "Reduced-motion, SSR, and registry guidance in one place.",
  },
];

const heroSignals = [
  {
    icon: Eye,
    title: "Readable first",
    description: "The final headline stays present in server-rendered HTML.",
  },
  {
    icon: ShieldCheck,
    title: "Motion aware",
    description:
      "Reduced-motion visitors get a calm static or opacity-only state.",
  },
  {
    icon: Gauge,
    title: "Fast by default",
    description:
      "Short stagger timing keeps the hero clear without delaying action.",
  },
  {
    icon: BookOpenText,
    title: "Easy to scan",
    description:
      "Icon and text pairs turn hero promises into quick proof points.",
  },
];

export function HeroTextAnimationProductionHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-production-heading"
        className="bg-background text-foreground border-border rounded-md border"
      >
        <div className="mx-auto max-w-5xl px-5 py-5 sm:px-8 lg:px-10">
          <header className="border-border bg-background/95 flex min-h-14 items-center gap-4 rounded-md border px-3 shadow-sm">
            <a
              href="#examples"
              aria-label="Dethink hero animation examples"
              className="focus-visible:ring-ring focus-visible:ring-offset-background flex min-w-0 shrink-0 items-center gap-2 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
            >
              <span
                aria-hidden="true"
                className="bg-primary text-primary-foreground grid size-8 shrink-0 place-items-center rounded-md"
              >
                <Sparkles className="size-4" />
              </span>
              <span className="font-heading hidden text-sm font-semibold sm:inline">
                Dethink Hero
              </span>
            </a>

            <nav
              aria-label="Hero recipe navigation"
              className="hidden min-w-0 flex-1 md:block"
            >
              <ul className="flex items-center justify-center gap-1">
                {navItems.map((item) => (
                  <li key={item.href}>
                    <a
                      href={item.href}
                      className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background inline-flex h-9 items-center rounded-md px-3 text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
                    >
                      {item.label}
                    </a>
                  </li>
                ))}
              </ul>
            </nav>

            <div className="ml-auto flex shrink-0 items-center gap-2">
              <DropdownMenu>
                <DropdownMenuTrigger
                  size="sm"
                  variant="ghost"
                  className="gap-1.5"
                >
                  Resources
                  <ChevronDown aria-hidden="true" className="size-3.5" />
                </DropdownMenuTrigger>
                <DropdownMenuContent placement="bottom end" showArrow>
                  <DropdownMenuSection>
                    <DropdownMenuLabel>Hero resources</DropdownMenuLabel>
                    {resourceItems.map(({ icon: Icon, label, description }) => (
                      <DropdownMenuItem key={label} textValue={label}>
                        <DropdownMenuItemIcon aria-hidden="true">
                          <Icon />
                        </DropdownMenuItemIcon>
                        <DropdownMenuItemLabel>{label}</DropdownMenuItemLabel>
                        <DropdownMenuItemDescription>
                          {description}
                        </DropdownMenuItemDescription>
                      </DropdownMenuItem>
                    ))}
                    <DropdownMenuSeparator />
                    <DropdownMenuItem textValue="Open production checklist">
                      <DropdownMenuItemIcon aria-hidden="true">
                        <BookOpenText />
                      </DropdownMenuItemIcon>
                      <DropdownMenuItemLabel>
                        Production checklist
                      </DropdownMenuItemLabel>
                    </DropdownMenuItem>
                  </DropdownMenuSection>
                </DropdownMenuContent>
              </DropdownMenu>

              <RevealButton
                className="hidden sm:inline-flex"
                icon={<PlayCircle />}
                label="Preview"
                size="sm"
                variant="outline"
              />
              <RevealButton
                icon={<Rocket />}
                label="Start"
                size="sm"
                variant="solid"
              />
            </div>
          </header>
        </div>

        <div className="mx-auto grid max-w-5xl gap-8 px-5 pt-6 pb-10 sm:px-8 lg:grid-cols-[minmax(0,1fr)_22rem] lg:items-center lg:px-10 lg:pt-10 lg:pb-14">
          <div className="min-w-0 space-y-7">
            <div className="border-border bg-muted/50 text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium">
              <Sparkles
                aria-hidden="true"
                className="text-primary size-3.5 shrink-0"
              />
              <span className="truncate">Hero section recipe</span>
            </div>

            <div className="space-y-5">
              <HeroTextAnimation
                id="hero-text-production-heading"
                repeat
                repeatDelay={1.6}
                text="Turn your launch headline into a clear first impression."
                className="font-heading text-foreground max-w-3xl text-4xl leading-[1.03] font-semibold tracking-normal sm:text-5xl lg:text-6xl"
              />
              <p className="text-muted-foreground max-w-xl text-base leading-7 sm:text-lg">
                Pair the animated headline with plain-language support copy,
                clear actions, and icon-led proof points so the hero stays
                usable before and after the motion completes.
              </p>
            </div>

            <div className="flex flex-col gap-3 sm:flex-row">
              <Button asChild size="lg" rightIcon={<ArrowRight />}>
                <a href="#installation">Install component</a>
              </Button>
              <Button asChild size="lg" variant="outline">
                <a href="#props">Review props</a>
              </Button>
            </div>
          </div>

          <ul className="grid min-w-0 gap-3 sm:grid-cols-2 lg:grid-cols-1">
            {heroSignals.map(({ icon: Icon, title, description }) => (
              <li
                key={title}
                className="border-border bg-muted/35 flex min-w-0 gap-3 rounded-md border p-4"
              >
                <span
                  aria-hidden="true"
                  className="bg-primary/10 text-primary grid size-10 shrink-0 place-items-center rounded-md"
                >
                  <Icon className="size-4" />
                </span>
                <span className="min-w-0">
                  <span className="block text-sm font-semibold">{title}</span>
                  <span className="text-muted-foreground mt-1 block text-sm leading-6">
                    {description}
                  </span>
                </span>
              </li>
            ))}
          </ul>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Design studio hero — masked curtain

An editorial agency layout where lines rise inside overflow masks: restrained typography, thin rules, and a numbered discipline index.

Studio Dethink

Independent design & motion studio

We design brands that move with intent.

A small studio for founders who care about the details. We shape identity, product, and motion into one deliberate, unhurried whole.

  • 01

    Brand systems

    Identity, voice, and design language built to scale.

  • 02

    Web & product

    Editorial marketing sites and considered product UI.

  • 03

    Motion & film

    Title sequences, launch films, and interface motion.

Show sourceexamples/hero-text-animation/masked-curtain-hero.tsx
examples/hero-text-animation/masked-curtain-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowUpRight } from "lucide-react";

const navItems = ["Work", "Studio", "Journal", "Contact"];

const disciplines = [
  {
    index: "01",
    title: "Brand systems",
    description: "Identity, voice, and design language built to scale.",
  },
  {
    index: "02",
    title: "Web & product",
    description: "Editorial marketing sites and considered product UI.",
  },
  {
    index: "03",
    title: "Motion & film",
    description: "Title sequences, launch films, and interface motion.",
  },
];

export function HeroTextAnimationMaskedCurtainHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-masked-curtain-heading"
        className="bg-background text-foreground border-border overflow-hidden rounded-md border"
      >
        <header className="border-border flex items-center justify-between gap-4 border-b px-5 py-4 sm:px-8 lg:px-12">
          <a
            href="#examples"
            className="font-heading focus-visible:ring-ring focus-visible:ring-offset-background rounded-sm text-xs font-semibold tracking-[0.28em] uppercase outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
          >
            Studio Dethink
          </a>
          <nav
            aria-label="Studio navigation"
            className="hidden items-center gap-7 md:flex"
          >
            {navItems.map((item) => (
              <a
                key={item}
                href="#examples"
                className="text-muted-foreground hover:text-foreground text-xs font-medium tracking-[0.14em] uppercase transition-colors"
              >
                {item}
              </a>
            ))}
          </nav>
          <span className="text-muted-foreground hidden text-xs tracking-[0.2em] uppercase sm:inline">
            Est. MMXXVI
          </span>
        </header>

        <div className="mx-auto max-w-3xl px-5 py-16 text-center sm:px-8 lg:py-24">
          <p className="text-muted-foreground text-xs font-medium tracking-[0.24em] uppercase">
            Independent design &amp; motion studio
          </p>
          <span
            aria-hidden="true"
            className="bg-border mx-auto mt-6 block h-px w-10"
          />
          <HeroTextAnimation
            animation="masked-curtain"
            ariaLabel="We design brands that move with intent."
            id="hero-text-masked-curtain-heading"
            repeat
            repeatDelay={1.8}
            text={"We design brands\nthat move with intent."}
            className="font-heading text-foreground mx-auto mt-8 max-w-3xl text-4xl leading-[1.05] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl"
          />
          <p className="text-muted-foreground mx-auto mt-7 max-w-xl text-base leading-7 sm:text-lg">
            A small studio for founders who care about the details. We shape
            identity, product, and motion into one deliberate, unhurried whole.
          </p>
          <div className="mt-9 flex flex-col items-center justify-center gap-4 sm:flex-row">
            <Button asChild size="lg" variant="outline">
              <a href="#installation">Start a project</a>
            </Button>
            <a
              href="#examples"
              className="text-foreground focus-visible:ring-ring focus-visible:ring-offset-background group inline-flex items-center gap-1.5 rounded-sm text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
            >
              View selected work
              <ArrowUpRight
                aria-hidden="true"
                className="size-4 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5"
              />
            </a>
          </div>
        </div>

        <ul className="border-border grid border-t sm:grid-cols-3">
          {disciplines.map(({ index, title, description }, position) => (
            <li
              key={title}
              className={`px-5 py-8 sm:px-8 lg:px-12 ${
                position > 0
                  ? "border-border border-t sm:border-t-0 sm:border-l"
                  : ""
              }`}
            >
              <span className="text-muted-foreground font-mono text-xs tracking-widest">
                {index}
              </span>
              <p className="font-heading mt-3 text-lg font-semibold">{title}</p>
              <p className="text-muted-foreground mt-1.5 text-sm leading-6">
                {description}
              </p>
            </li>
          ))}
        </ul>
      </section>
    </HeroTextAnimationProvider>
  );
}

Developer tool hero — typewriter

A CLI/DX layout pairing the typed headline with a live terminal window, install command, and GitHub actions.

v2.0.0 — now stable

Ship your CLI in an afternoon.

A typed, developer-first toolkit for building command-line tools your team actually enjoys. Zero config, fully tree-shakeable.

$ npm i -D @dethink/cli
~/app — zsh

npx dethink init landing

◇ Scaffolding hero, motion tokens, registry…

◇ Wiring reduced-motion + SSR fallbacks…

✓ Ready in 1.2s — run pnpm dev

node v22.4.0Done
Show sourceexamples/hero-text-animation/typewriter-hero.tsx
examples/hero-text-animation/typewriter-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import {
  ArrowRight,
  BookOpen,
  Check,
  Copy,
  GitBranch,
  Star,
  Terminal,
} from "lucide-react";

const terminalLines = [
  { prompt: true, text: "npx dethink init landing" },
  { prompt: false, text: "◇ Scaffolding hero, motion tokens, registry…" },
  { prompt: false, text: "◇ Wiring reduced-motion + SSR fallbacks…" },
  { prompt: false, text: "✓ Ready in 1.2s — run pnpm dev", ok: true },
];

export function HeroTextAnimationTypewriterHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-typewriter-heading"
        className="bg-background text-foreground border-border overflow-hidden rounded-md border"
      >
        <div className="mx-auto grid max-w-5xl items-center gap-10 px-5 py-12 sm:px-8 lg:grid-cols-[minmax(0,1fr)_26rem] lg:px-10 lg:py-16">
          <div className="min-w-0">
            <span className="border-border bg-muted/50 text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 font-mono text-xs font-medium">
              <span
                aria-hidden="true"
                className="bg-success size-1.5 rounded-full"
              />
              v2.0.0 — now stable
            </span>

            <HeroTextAnimation
              animation="typewriter"
              duration={1.9}
              id="hero-text-typewriter-heading"
              repeat
              repeatDelay={2.2}
              text="Ship your CLI in an afternoon."
              className="font-heading text-foreground mt-6 max-w-xl text-4xl leading-[1.05] font-semibold tracking-tight sm:text-5xl lg:text-6xl"
            />
            <p className="text-muted-foreground mt-6 max-w-md text-base leading-7 sm:text-lg">
              A typed, developer-first toolkit for building command-line tools
              your team actually enjoys. Zero config, fully tree-shakeable.
            </p>

            <div className="border-border bg-muted/40 mt-7 flex max-w-md items-center gap-3 rounded-md border px-3.5 py-2.5">
              <Terminal
                aria-hidden="true"
                className="text-muted-foreground size-4 shrink-0"
              />
              <code className="text-foreground min-w-0 flex-1 truncate font-mono text-sm">
                <span className="text-muted-foreground select-none">$ </span>
                npm i -D @dethink/cli
              </code>
              <button
                type="button"
                aria-label="Copy install command"
                className="text-muted-foreground hover:text-foreground hover:bg-background focus-visible:ring-ring focus-visible:ring-offset-background grid size-7 shrink-0 place-items-center rounded outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
              >
                <Copy aria-hidden="true" className="size-3.5" />
              </button>
            </div>

            <div className="mt-7 flex flex-col gap-3 sm:flex-row sm:items-center">
              <Button asChild size="lg" leftIcon={<BookOpen />}>
                <a href="#installation">Read the docs</a>
              </Button>
              <Button
                asChild
                size="lg"
                variant="ghost"
                leftIcon={<GitBranch />}
                rightIcon={
                  <span className="text-muted-foreground inline-flex items-center gap-1 text-xs">
                    <Star aria-hidden="true" className="size-3.5" />
                    12.4k
                  </span>
                }
              >
                <a href="#examples">Star on GitHub</a>
              </Button>
            </div>
          </div>

          <div className="bg-foreground text-background min-w-0 overflow-hidden rounded-lg shadow-lg">
            <div className="border-background/15 flex items-center gap-2 border-b px-4 py-3">
              <span className="flex items-center gap-1.5" aria-hidden="true">
                <span className="bg-destructive size-3 rounded-full" />
                <span className="bg-warning size-3 rounded-full" />
                <span className="bg-success size-3 rounded-full" />
              </span>
              <span className="text-background/60 ml-2 font-mono text-xs">
                ~/app — zsh
              </span>
            </div>
            <div className="space-y-2 px-4 py-5 font-mono text-[13px] leading-relaxed">
              {terminalLines.map((line, index) => (
                <p
                  key={index}
                  className={
                    line.ok
                      ? "text-background flex items-start gap-2"
                      : line.prompt
                        ? "text-background flex items-start gap-2"
                        : "text-background/65 flex items-start gap-2"
                  }
                >
                  {line.prompt ? (
                    <span aria-hidden="true" className="text-background/50">
                      $
                    </span>
                  ) : line.ok ? (
                    <Check
                      aria-hidden="true"
                      className="text-success mt-0.5 size-3.5 shrink-0"
                    />
                  ) : (
                    <span aria-hidden="true" className="text-background/30">

                    </span>
                  )}
                  <span className="min-w-0">{line.text}</span>
                </p>
              ))}
              <p className="text-background flex items-center gap-2 pt-1 font-mono">
                <span aria-hidden="true" className="text-background/50">
                  $
                </span>
                <span
                  aria-hidden="true"
                  className="bg-background inline-block h-4 w-2 animate-pulse motion-reduce:animate-none"
                />
              </p>
            </div>
            <div className="border-background/15 text-background/60 flex items-center justify-between border-t px-4 py-2.5 font-mono text-xs">
              <span>node v22.4.0</span>
              <span className="inline-flex items-center gap-1.5">
                Done
                <ArrowRight aria-hidden="true" className="size-3" />
              </span>
            </div>
          </div>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Security platform hero — scramble decrypt

A zero-trust layout with a grid backdrop, compliance chips, and a live encrypted-status console beside the decrypting headline.

End-to-end encrypted by default

Decrypt threats before they move.

Runtime detection that resolves noise into signal. Continuous posture, automated key rotation, and audit trails your security team can actually read.

SOC 2 Type IIISO 27001Zero-trust
All systems encryptedlive
  • handshake — TLS 1.3 established
  • identity — device attested
  • keys — rotated 4m ago
  • secret — sk_live_••••••••••••4e2a
audit log · 2,481 events today
Show sourceexamples/hero-text-animation/scramble-decrypt-hero.tsx
examples/hero-text-animation/scramble-decrypt-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import type { CSSProperties } from "react";
import {
  ArrowRight,
  Fingerprint,
  KeyRound,
  Lock,
  ShieldCheck,
} from "lucide-react";

const gridStyle: CSSProperties = {
  backgroundImage:
    "linear-gradient(var(--dt-color-border) 1px, transparent 1px), linear-gradient(90deg, var(--dt-color-border) 1px, transparent 1px)",
  backgroundSize: "2.75rem 2.75rem",
  maskImage: "radial-gradient(120% 90% at 30% 0%, black, transparent 75%)",
  WebkitMaskImage:
    "radial-gradient(120% 90% at 30% 0%, black, transparent 75%)",
};

const certifications = [
  { icon: ShieldCheck, label: "SOC 2 Type II" },
  { icon: Lock, label: "ISO 27001" },
  { icon: Fingerprint, label: "Zero-trust" },
];

const logLines = [
  { tone: "info", text: "handshake — TLS 1.3 established" },
  { tone: "info", text: "identity — device attested" },
  { tone: "ok", text: "keys — rotated 4m ago" },
  { tone: "muted", text: "secret — sk_live_••••••••••••4e2a" },
];

export function HeroTextAnimationScrambleDecryptHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-scramble-heading"
        className="bg-background text-foreground border-border relative overflow-hidden rounded-md border"
      >
        <span
          aria-hidden="true"
          style={gridStyle}
          className="pointer-events-none absolute inset-0 opacity-60"
        />

        <div className="relative mx-auto grid max-w-5xl items-center gap-10 px-5 py-14 sm:px-8 lg:grid-cols-[minmax(0,1fr)_22rem] lg:px-10 lg:py-20">
          <div className="min-w-0">
            <span className="border-border bg-background/70 text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 font-mono text-xs backdrop-blur">
              <KeyRound aria-hidden="true" className="text-info size-3.5" />
              End-to-end encrypted by default
            </span>

            <HeroTextAnimation
              animation="scramble-decrypt"
              duration={1}
              id="hero-text-scramble-heading"
              repeat
              repeatDelay={1.8}
              text="Decrypt threats before they move."
              className="font-heading text-foreground mt-6 max-w-2xl text-4xl leading-[1.05] font-semibold tracking-tight sm:text-5xl lg:text-6xl"
            />
            <p className="text-muted-foreground mt-6 max-w-lg text-base leading-7 sm:text-lg">
              Runtime detection that resolves noise into signal. Continuous
              posture, automated key rotation, and audit trails your security
              team can actually read.
            </p>

            <div className="mt-7 flex flex-wrap items-center gap-2.5">
              {certifications.map(({ icon: Icon, label }) => (
                <span
                  key={label}
                  className="border-border bg-background/70 text-foreground inline-flex items-center gap-2 rounded-md border px-3 py-1.5 font-mono text-xs font-medium"
                >
                  <Icon
                    aria-hidden="true"
                    className="text-muted-foreground size-3.5"
                  />
                  {label}
                </span>
              ))}
            </div>

            <div className="mt-8 flex flex-col gap-3 sm:flex-row">
              <Button asChild size="lg" rightIcon={<ArrowRight />}>
                <a href="#installation">Request access</a>
              </Button>
              <Button asChild size="lg" variant="outline">
                <a href="#props">Read the security model</a>
              </Button>
            </div>
          </div>

          <div className="border-border bg-background/80 min-w-0 rounded-lg border shadow-sm backdrop-blur">
            <div className="border-border flex items-center justify-between border-b px-4 py-3">
              <span className="inline-flex items-center gap-2 font-mono text-xs font-medium">
                <span className="relative flex size-2">
                  <span className="bg-success/60 absolute inline-flex size-full animate-ping rounded-full motion-reduce:hidden" />
                  <span className="bg-success relative inline-flex size-2 rounded-full" />
                </span>
                All systems encrypted
              </span>
              <span className="text-muted-foreground font-mono text-xs">
                live
              </span>
            </div>
            <ul className="space-y-3 px-4 py-4 font-mono text-xs">
              {logLines.map(({ tone, text }, index) => (
                <li key={index} className="flex items-center gap-2.5">
                  <span
                    aria-hidden="true"
                    className={`size-1.5 shrink-0 rounded-full ${
                      tone === "ok"
                        ? "bg-success"
                        : tone === "info"
                          ? "bg-info"
                          : "bg-muted-foreground/50"
                    }`}
                  />
                  <span
                    className={
                      tone === "muted"
                        ? "text-muted-foreground truncate"
                        : "text-foreground truncate"
                    }
                  >
                    {text}
                  </span>
                </li>
              ))}
            </ul>
            <div className="border-border text-muted-foreground border-t px-4 py-2.5 font-mono text-[11px]">
              audit log · 2,481 events today
            </div>
          </div>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Multi-audience hero — rotating keyword

A product-led layout that swaps the audience keyword while keeping one stable accessible sentence, audience chips, and a team avatar cloud.

One builder · every team

Launch pages for startups, agencies, product teams, and founders.

One flexible page builder that speaks every team's language. Ship a launch that fits the audience — without a redesign for each one.

Trusted across 12 departments at 4,000+ companies

Show sourceexamples/hero-text-animation/rotating-keyword-hero.tsx
examples/hero-text-animation/rotating-keyword-hero.tsx
"use client";

import { useEffect, useState } from "react";
import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowRight, Play } from "lucide-react";

const audiences = ["Startups", "Agencies", "Product teams", "Founders"];

// The trailing period is baked into each keyword (rather than a separate
// rotatingKeywordSuffix) so it rides the keyword's own text baseline instead of
// the overflow-clipped rotating slot's synthesized baseline, which otherwise
// dropped the "." below the line with an odd gap.
const rotatingKeywords = [
  "startups.",
  "agencies.",
  "product teams.",
  "founders.",
];
const rotateIntervalMs = 2000;

const teamAvatars = [
  { initials: "SO", tone: "bg-primary/15 text-primary" },
  { initials: "CS", tone: "bg-info/15 text-info" },
  { initials: "FL", tone: "bg-success/15 text-success" },
  { initials: "PM", tone: "bg-warning/20 text-warning" },
  { initials: "OP", tone: "bg-muted text-muted-foreground" },
];

export function HeroTextAnimationRotatingKeywordHero() {
  const [keywordIndex, setKeywordIndex] = useState(0);

  // Loop the keyword indefinitely for the showcase. The component's built-in
  // autoRotateKeywords stops after ~5s (a WCAG 2.2.2 guard against text that
  // moves forever), so we drive the index ourselves in controlled mode to keep
  // the demo playing — while still honoring prefers-reduced-motion.
  useEffect(() => {
    if (typeof window === "undefined") {
      return;
    }

    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      return;
    }

    const intervalId = window.setInterval(() => {
      setKeywordIndex((index) => (index + 1) % rotatingKeywords.length);
    }, rotateIntervalMs);

    return () => window.clearInterval(intervalId);
  }, []);

  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-rotating-keyword-heading"
        className="bg-background text-foreground border-border overflow-hidden rounded-md border"
      >
        <div className="mx-auto max-w-3xl px-5 py-16 text-center sm:px-8 lg:py-20">
          <span className="border-border bg-muted/50 text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium">
            <span
              aria-hidden="true"
              className="bg-primary size-1.5 rounded-full"
            />
            One builder · every team
          </span>

          <HeroTextAnimation
            animation="rotating-keyword"
            ariaLabel="Launch pages for startups, agencies, product teams, and founders."
            duration={0.34}
            id="hero-text-rotating-keyword-heading"
            rotatingKeywordIndex={keywordIndex}
            rotatingKeywordOptions={rotatingKeywords}
            rotatingKeywordPrefix="Launch pages for "
            text="Launch pages for startups, agencies, product teams, and founders."
            className="font-heading text-foreground [&_[data-slot=hero-text-animation-rotating-keyword]]:text-primary mx-auto mt-7 max-w-2xl text-4xl leading-[1.06] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl [&_[data-slot=hero-text-animation-motion]]:justify-center"
          />

          <p className="text-muted-foreground mx-auto mt-6 max-w-xl text-base leading-7 sm:text-lg">
            One flexible page builder that speaks every team&apos;s language.
            Ship a launch that fits the audience — without a redesign for each
            one.
          </p>

          <div
            className="mt-8 flex flex-wrap items-center justify-center gap-2"
            aria-hidden="true"
          >
            <span className="text-muted-foreground mr-1 text-xs font-medium tracking-wide uppercase">
              Built for
            </span>
            {audiences.map((audience) => (
              <span
                key={audience}
                className="border-border bg-background text-foreground inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium"
              >
                {audience}
              </span>
            ))}
          </div>

          <div className="mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation">Start building free</a>
            </Button>
            <Button asChild size="lg" variant="ghost" leftIcon={<Play />}>
              <a href="#examples">Watch the tour</a>
            </Button>
          </div>

          <div className="mt-10 flex items-center justify-center gap-3">
            <ul className="flex -space-x-2" aria-hidden="true">
              {teamAvatars.map(({ initials, tone }) => (
                <li
                  key={initials}
                  className={`border-background grid size-8 place-items-center rounded-full border-2 text-[11px] font-semibold ${tone}`}
                >
                  {initials}
                </li>
              ))}
            </ul>
            <p className="text-muted-foreground text-sm">
              Trusted across 12 departments at 4,000+ companies
            </p>
          </div>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

AI product hero — gradient highlight

A brand-forward, centered layout with soft glow backdrops, a one-shot gradient sweep, and an early-access email capture.

New — AI that writes in your voice

Write copy that sounds unmistakably you.

Trained on your best pages, our model drafts launch copy, ads, and emails that stay on-brand — then hands you the edit, never the other way around.

  • No credit card
  • 2-minute setup
  • Cancel anytime
Show sourceexamples/hero-text-animation/gradient-highlight-hero.tsx
examples/hero-text-animation/gradient-highlight-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { Check, Sparkles } from "lucide-react";

const reassurances = ["No credit card", "2-minute setup", "Cancel anytime"];

export function HeroTextAnimationGradientHighlightHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-gradient-highlight-heading"
        className="bg-background text-foreground border-border relative isolate overflow-hidden rounded-md border"
      >
        <span
          aria-hidden="true"
          className="bg-primary/20 pointer-events-none absolute -top-24 left-1/2 -z-10 size-72 -translate-x-1/2 rounded-full blur-3xl"
        />
        <span
          aria-hidden="true"
          className="bg-info/20 pointer-events-none absolute -top-10 right-10 -z-10 size-56 rounded-full blur-3xl"
        />

        <div className="mx-auto max-w-3xl px-5 py-16 text-center sm:px-8 lg:py-24">
          <span className="border-border bg-background/70 text-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium backdrop-blur">
            <Sparkles aria-hidden="true" className="text-primary size-3.5" />
            New — AI that writes in your voice
          </span>

          <HeroTextAnimation
            animation="gradient-highlight"
            duration={1.8}
            id="hero-text-gradient-highlight-heading"
            repeat
            repeatDelay={2}
            text="Write copy that sounds unmistakably you."
            className="font-heading text-foreground mx-auto mt-7 max-w-2xl text-4xl leading-[1.05] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl"
          />

          <p className="text-muted-foreground mx-auto mt-6 max-w-xl text-base leading-7 sm:text-lg">
            Trained on your best pages, our model drafts launch copy, ads, and
            emails that stay on-brand — then hands you the edit, never the other
            way around.
          </p>

          <div className="mx-auto mt-9 flex max-w-md flex-col gap-2.5 sm:flex-row">
            <label htmlFor="gradient-hero-email" className="sr-only">
              Work email
            </label>
            <input
              id="gradient-hero-email"
              type="email"
              inputMode="email"
              placeholder="you@company.com"
              className="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring focus-visible:ring-offset-background h-11 min-w-0 flex-1 rounded-md border px-4 text-sm outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
            />
            <Button size="lg" className="shrink-0">
              Get early access
            </Button>
          </div>

          <ul className="text-muted-foreground mt-5 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm">
            {reassurances.map((item) => (
              <li key={item} className="inline-flex items-center gap-1.5">
                <Check aria-hidden="true" className="text-success size-4" />
                {item}
              </li>
            ))}
          </ul>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Luxury launch hero — blur focus

A cinematic, minimal product-launch layout with generous whitespace, a soft vignette backdrop, and a single film CTA.

Introducing

Aura · Series 02

Designed to disappear into your day.

Ambient light that reads the room and fades from view. Nothing to configure. Nothing to notice — until you would miss it.

Available Fall 2026Reserve yours
Show sourceexamples/hero-text-animation/blur-focus-hero.tsx
examples/hero-text-animation/blur-focus-hero.tsx
"use client";

import type { CSSProperties } from "react";
import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowRight, Play } from "lucide-react";

const backdropStyle: CSSProperties = {
  backgroundImage:
    "radial-gradient(80% 60% at 50% 0%, color-mix(in oklab, var(--dt-color-primary) 14%, transparent), transparent 70%), linear-gradient(to bottom, color-mix(in oklab, var(--dt-color-foreground) 5%, transparent), transparent 40%)",
};

export function HeroTextAnimationBlurFocusHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-blur-focus-heading"
        className="bg-background text-foreground border-border relative overflow-hidden rounded-md border"
      >
        <span
          aria-hidden="true"
          style={backdropStyle}
          className="pointer-events-none absolute inset-0"
        />

        <div className="relative mx-auto flex max-w-2xl flex-col items-center px-5 py-20 text-center sm:px-8 lg:py-28">
          <p className="text-muted-foreground text-xs font-medium tracking-[0.32em] uppercase">
            Introducing
          </p>
          <p className="font-heading text-foreground mt-3 text-sm font-semibold tracking-[0.4em] uppercase">
            Aura · Series 02
          </p>

          <HeroTextAnimation
            animation="blur-focus"
            duration={1.2}
            id="hero-text-blur-focus-heading"
            repeat
            repeatDelay={2.4}
            text="Designed to disappear into your day."
            className="font-heading text-foreground mt-9 max-w-2xl text-4xl leading-[1.05] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl"
          />

          <p className="text-muted-foreground mt-7 max-w-md text-base leading-7 sm:text-lg">
            Ambient light that reads the room and fades from view. Nothing to
            configure. Nothing to notice — until you would miss it.
          </p>

          <div className="mt-9">
            <Button asChild size="lg" variant="outline" leftIcon={<Play />}>
              <a href="#examples">Watch the film</a>
            </Button>
          </div>
        </div>

        <div className="border-border relative flex flex-col items-center justify-between gap-3 border-t px-5 py-5 text-sm sm:flex-row sm:px-8 lg:px-12">
          <span className="text-muted-foreground tracking-wide">
            Available Fall 2026
          </span>
          <a
            href="#installation"
            className="text-foreground focus-visible:ring-ring focus-visible:ring-offset-background group inline-flex items-center gap-1.5 rounded-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
          >
            Reserve yours
            <ArrowRight
              aria-hidden="true"
              className="size-4 transition-transform group-hover:translate-x-0.5"
            />
          </a>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Photography studio hero — word-by-word blur focus

The same blur-focus reveal split per word: each word racks from soft focus into clarity in sequence via the splitBy word prop, in an editorial studio layout.

Aperture Studio

Every frame pulled into perfect focus.

A photography practice for brands that sweat the details. Each word racks into focus the way our lenses do — deliberately, one plane at a time, until the whole picture is sharp.

Word-by-word focus
Show sourceexamples/hero-text-animation/word-blur-focus-hero.tsx
examples/hero-text-animation/word-blur-focus-hero.tsx
"use client";

import type { CSSProperties } from "react";
import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowRight, Aperture } from "lucide-react";

const backdropStyle: CSSProperties = {
  backgroundImage:
    "radial-gradient(120% 80% at 15% 0%, color-mix(in oklab, var(--dt-color-primary) 12%, transparent), transparent 60%), linear-gradient(to bottom, color-mix(in oklab, var(--dt-color-foreground) 6%, transparent), transparent 45%)",
};

export function HeroTextAnimationWordBlurFocusHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-word-blur-focus-heading"
        className="bg-background text-foreground border-border relative overflow-hidden rounded-md border"
      >
        <span
          aria-hidden="true"
          style={backdropStyle}
          className="pointer-events-none absolute inset-0"
        />

        <div className="relative mx-auto flex max-w-3xl flex-col px-5 py-20 sm:px-8 lg:py-28">
          <p className="text-muted-foreground inline-flex items-center gap-2 text-xs font-medium tracking-[0.32em] uppercase">
            <Aperture aria-hidden="true" className="text-primary size-4" />
            Aperture Studio
          </p>

          <HeroTextAnimation
            animation="blur-focus"
            splitBy="word"
            duration={0.7}
            stagger={0.09}
            id="hero-text-word-blur-focus-heading"
            repeat
            repeatDelay={2.2}
            text="Every frame pulled into perfect focus."
            className="font-heading text-foreground mt-8 max-w-2xl text-4xl leading-[1.05] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl"
          />

          <p className="text-muted-foreground mt-7 max-w-lg text-base leading-7 sm:text-lg">
            A photography practice for brands that sweat the details. Each word
            racks into focus the way our lenses do — deliberately, one plane at
            a time, until the whole picture is sharp.
          </p>

          <div className="mt-9 flex flex-col gap-3 sm:flex-row sm:items-center">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation">Book a session</a>
            </Button>
            <a
              href="#examples"
              className="text-foreground focus-visible:ring-ring focus-visible:ring-offset-background group inline-flex items-center gap-1.5 rounded-sm text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
            >
              View the portfolio
              <ArrowRight
                aria-hidden="true"
                className="size-4 transition-transform group-hover:translate-x-0.5"
              />
            </a>
          </div>
        </div>

        <div className="border-border text-muted-foreground relative flex items-center justify-between border-t px-5 py-3.5 text-xs tracking-[0.18em] uppercase sm:px-8">
          <span>Word-by-word focus</span>
          <span aria-hidden="true">Soft → sharp</span>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Conversion hero — kinetic emphasis pop

A marketing layout that pops key value words (reinforced by weight, color, and underline) above a star-rating proof line and a metric row.

Rated 4.9/5 by 4,000+ growth teams

Turn more visitors into revenue, faster.

Run experiments on every headline, offer, and CTA — then let the winners ship themselves. Built for teams that grow on evidence, not opinions.

2.4×
more pipeline from the same traffic
−38%
wasted ad spend in 60 days
6 min
to launch your first test
Show sourceexamples/hero-text-animation/kinetic-emphasis-pop-hero.tsx
examples/hero-text-animation/kinetic-emphasis-pop-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowRight, Star, TrendingUp } from "lucide-react";

const metrics = [
  { value: "2.4×", label: "more pipeline from the same traffic" },
  { value: "−38%", label: "wasted ad spend in 60 days" },
  { value: "6 min", label: "to launch your first test" },
];

export function HeroTextAnimationKineticEmphasisPopHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-kinetic-emphasis-heading"
        className="bg-background text-foreground border-border overflow-hidden rounded-md border"
      >
        <div className="mx-auto max-w-3xl px-5 py-16 text-center sm:px-8 lg:py-20">
          <div className="text-muted-foreground flex items-center justify-center gap-2 text-sm">
            <span className="flex" aria-hidden="true">
              {Array.from({ length: 5 }).map((_, index) => (
                <Star
                  key={index}
                  className="fill-warning text-warning size-4"
                />
              ))}
            </span>
            <span>
              Rated <span className="text-foreground font-semibold">4.9/5</span>{" "}
              by 4,000+ growth teams
            </span>
          </div>

          <HeroTextAnimation
            animation="kinetic-emphasis-pop"
            duration={0.5}
            stagger={0.07}
            emphasisWords={["revenue", "faster"]}
            id="hero-text-kinetic-emphasis-heading"
            repeat
            repeatDelay={2}
            text="Turn more visitors into revenue, faster."
            className="font-heading text-foreground mx-auto mt-6 max-w-2xl text-4xl leading-[1.06] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl"
          />

          <p className="text-muted-foreground mx-auto mt-6 max-w-xl text-base leading-7 sm:text-lg">
            Run experiments on every headline, offer, and CTA — then let the
            winners ship themselves. Built for teams that grow on evidence, not
            opinions.
          </p>

          <div className="mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation">Start free trial</a>
            </Button>
            <Button asChild size="lg" variant="outline">
              <a href="#examples">Book a demo</a>
            </Button>
          </div>
        </div>

        <dl className="border-border grid border-t sm:grid-cols-3">
          {metrics.map(({ value, label }, position) => (
            <div
              key={label}
              className={`px-5 py-8 text-center sm:px-8 ${
                position > 0
                  ? "border-border border-t sm:border-t-0 sm:border-l"
                  : ""
              }`}
            >
              <dt className="text-primary font-heading inline-flex items-center gap-1.5 text-3xl font-semibold tracking-tight sm:text-4xl">
                {position === 0 ? (
                  <TrendingUp aria-hidden="true" className="size-6" />
                ) : null}
                {value}
              </dt>
              <dd className="text-muted-foreground mx-auto mt-2 max-w-[16rem] text-sm leading-6">
                {label}
              </dd>
            </div>
          ))}
        </dl>
      </section>
    </HeroTextAnimationProvider>
  );
}

Campaign poster hero — SVG stroke draw

A design-forward poster layout with edition markers and a wordmark whose letterforms trace their outline and then fill, with the real heading kept available to assistive technology.

Dethink№ 01

The 2026 brand refresh

Make it unforgettable.

A wordmark that draws itself, a palette that shifts with the light, and a system built to carry a brand across every surface.

Design-forward hero
Show sourceexamples/hero-text-animation/svg-stroke-draw-hero.tsx
examples/hero-text-animation/svg-stroke-draw-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowRight } from "lucide-react";

export function HeroTextAnimationSvgStrokeDrawHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-svg-stroke-draw-heading"
        className="bg-background text-foreground border-border overflow-hidden rounded-md border"
      >
        <div className="border-border text-muted-foreground grid grid-cols-3 border-b text-xs font-medium tracking-[0.22em] uppercase">
          <span className="px-5 py-3.5 sm:px-8">Dethink</span>
          <span className="border-border hidden border-x px-5 py-3.5 text-center sm:block">
            Campaign · 2026
          </span>
          <span className="px-5 py-3.5 text-right sm:px-8">№ 01</span>
        </div>

        <div className="mx-auto max-w-3xl px-5 py-20 text-center sm:px-8 lg:py-28">
          <p className="text-muted-foreground text-xs font-medium tracking-[0.3em] uppercase">
            The 2026 brand refresh
          </p>

          <HeroTextAnimation
            animation="svg-stroke-draw"
            duration={1.4}
            id="hero-text-svg-stroke-draw-heading"
            text={"Make it\nunforgettable."}
            className="font-heading text-foreground mx-auto mt-8 max-w-2xl font-semibold tracking-tight"
          />

          <p className="text-muted-foreground mx-auto mt-10 max-w-md text-base leading-7 sm:text-lg">
            A wordmark that draws itself, a palette that shifts with the light,
            and a system built to carry a brand across every surface.
          </p>

          <div className="mt-9">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation">See the identity system</a>
            </Button>
          </div>
        </div>

        <div className="border-border text-muted-foreground flex items-center justify-between border-t px-5 py-3.5 text-xs tracking-[0.18em] uppercase sm:px-8">
          <span>Design-forward hero</span>
          <span aria-hidden="true">Outline → fill</span>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Storytelling hero — scroll responsive

A long-scroll narrative layout with chapter markers and a scroll cue, where the opening line subtly recedes on scroll while staying readable.

A field guide · Chapter 01

Every product starts as a quiet, stubborn idea.

Scroll and the opening line steps back, making room for the story. Nothing essential moves out of reach — the heading stays readable the whole way down.

Scroll to begin

01 — The quiet idea

It rarely arrives as a plan. It shows up as a small, nagging sense that something could be better — and refuses to leave until you build it.

Show sourceexamples/hero-text-animation/scroll-responsive-hero.tsx
examples/hero-text-animation/scroll-responsive-hero.tsx
"use client";

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import { ArrowRight, ChevronDown } from "lucide-react";

const chapters = [
  { index: "01", title: "The quiet idea", active: true },
  { index: "02", title: "The stubborn build" },
  { index: "03", title: "The first believer" },
];

export function HeroTextAnimationScrollResponsiveHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-scroll-responsive-heading"
        className="bg-background text-foreground border-border overflow-hidden rounded-md border"
      >
        <div className="mx-auto grid max-w-5xl gap-10 px-5 py-16 sm:px-8 lg:grid-cols-[minmax(0,1fr)_16rem] lg:px-10 lg:py-24">
          <div className="min-w-0">
            <p className="text-muted-foreground text-xs font-medium tracking-[0.24em] uppercase">
              A field guide · Chapter 01
            </p>

            <HeroTextAnimation
              animation="scroll-responsive"
              id="hero-text-scroll-responsive-heading"
              text="Every product starts as a quiet, stubborn idea."
              className="font-heading text-foreground mt-6 max-w-2xl text-4xl leading-[1.06] font-semibold tracking-tight text-balance sm:text-5xl lg:text-6xl"
            />

            <p className="text-muted-foreground mt-7 max-w-lg text-base leading-7 sm:text-lg">
              Scroll and the opening line steps back, making room for the story.
              Nothing essential moves out of reach — the heading stays readable
              the whole way down.
            </p>

            <div className="mt-8">
              <Button
                asChild
                size="lg"
                variant="outline"
                rightIcon={<ArrowRight />}
              >
                <a href="#examples">Read the story</a>
              </Button>
            </div>

            <p className="text-muted-foreground mt-12 inline-flex items-center gap-2 text-xs font-medium tracking-[0.18em] uppercase">
              <ChevronDown
                aria-hidden="true"
                className="size-4 animate-bounce motion-reduce:animate-none"
              />
              Scroll to begin
            </p>
          </div>

          <nav
            aria-label="Chapters"
            className="border-border lg:border-l lg:pl-8"
          >
            <p className="text-muted-foreground text-xs font-medium tracking-[0.2em] uppercase">
              In this story
            </p>
            <ol className="mt-4 space-y-4">
              {chapters.map(({ index, title, active }) => (
                <li key={index} className="flex items-start gap-3">
                  <span
                    aria-hidden="true"
                    className={`mt-1.5 size-1.5 shrink-0 rounded-full ${
                      active ? "bg-primary" : "bg-border"
                    }`}
                  />
                  <span className="min-w-0">
                    <span
                      className={`font-mono text-xs ${
                        active ? "text-primary" : "text-muted-foreground"
                      }`}
                    >
                      {index}
                    </span>
                    <span
                      className={`mt-0.5 block text-sm font-medium ${
                        active ? "text-foreground" : "text-muted-foreground"
                      }`}
                    >
                      {title}
                    </span>
                  </span>
                </li>
              ))}
            </ol>
          </nav>
        </div>

        <div className="border-border bg-muted/30 border-t px-5 py-8 sm:px-8 lg:px-10">
          <div className="mx-auto max-w-2xl">
            <p className="text-muted-foreground font-mono text-xs tracking-widest">
              01 — The quiet idea
            </p>
            <p className="text-foreground mt-3 text-lg leading-8">
              It rarely arrives as a plan. It shows up as a small, nagging sense
              that something could be better — and refuses to leave until you
              build it.
            </p>
          </div>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

HeroTextAnimation renders real text and uses a decorative visual layer only for the animation.

Avoid scramble and decrypt effects for legal, medical, pricing, or compliance-critical wording. Use static text for copy where every character must be immediately reviewable. Limit blur focus to short cinematic hero headings that can finish resolving before users need to read the copy. Use SVG stroke draw for short headings only; it renders scalable letterforms that trace and fill, does not auto-wrap, and keeps the real heading available to assistive technology. Use scroll-responsive text only when the full heading is readable before the first scroll and the next content remains in normal document flow.

HeroTextAnimation props
PropWhat it doesDefault
textstringFinal hero copy. It is rendered as stable accessible text and split only for the visual animation layer.Not set
animation"stagger-words" | "masked-curtain" | "typewriter" | "scramble-decrypt" | "rotating-keyword" | "gradient-highlight" | "blur-focus" | "kinetic-emphasis-pop" | "svg-stroke-draw" | "scroll-responsive"Animation style. Use staggered words for the safest default, masked curtain for line-by-line reveals, typewriter for short developer/product hero copy, scramble-decrypt for deterministic decorative glyph resolution, rotating-keyword for a stable sentence with a decorative swapping slot, gradient-highlight for a one-shot tokenized highlight sweep over a complete phrase, blur-focus for short cinematic hero headings that resolve quickly into crisp readable text, kinetic-emphasis-pop for one or two statically emphasized words with a subtle one-shot scale accent, svg-stroke-draw for a scalable letterform outline-to-fill trace on short headings, or scroll-responsive for a subtle bounded first-scroll response on storytelling pages."stagger-words"
as"h1" | "h2" | "p" | "span"Semantic element for the root. Use h1 or h2 for actual hero headings."h1"
ariaLabelstringStable label announced by assistive technology when the visual fragments need different copy.text
splitBy"word" | "line"Controls whether the visual layer staggers each word or explicit newline-delimited lines."word"
trigger"mount" | "in-view" | "manual"Runs on mount, when the hero enters the viewport, or from the controlled active state."mount"
reducedMotionStrategy"static" | "opacity-only"Controls the fallback when reduced motion is active. Transform motion is removed either way."opacity-only"
showCaretbooleanShows a decorative typewriter caret while typing. The caret is removed after completion and never loops.true
repeatbooleanReplays the decorative visual animation after it completes. Keep it off for static content and enable it for demos or controlled preview surfaces.false
repeatDelaynumberDelay in seconds before an enabled repeat starts the animation again. Reduced-motion rendering never schedules repeats.1.8
rotatingKeywordOptionsreadonly string[]Keyword or short phrase options for the rotating-keyword visual slot. The slot reserves the longest option so the headline layout stays stable.[text]
rotatingKeywordPrefix / rotatingKeywordSuffixstringVisible decorative text rendered before and after the keyword slot. Keep text as the complete stable sentence for assistive technology."" / ""
rotatingKeywordIndex / defaultRotatingKeywordIndexnumberControls or initializes the active keyword. Use rotatingKeywordIndex with onRotatingKeywordIndexChange for manual selectors.undefined / 0
autoRotateKeywords / rotatingKeywordIntervalboolean / numberOpt-in keyword auto rotation. Timers are bounded and stop within five seconds; reduced-motion rendering never schedules them.false / 1.6
emphasisWordsreadonly string[]Word terms to emphasize for kinetic-emphasis-pop. Matching is case-insensitive and ignores surrounding punctuation. The component uses the first two resolved words.[]
emphasisWordIndicesreadonly number[]Zero-based word indices to emphasize for kinetic-emphasis-pop. Indices resolve before emphasisWords so precise targeting can override repeated terms.[]
svgPathDatastring | readonly string[] | readonly { d: string; strokeWidth?: number }[]Deprecated. svg-stroke-draw now traces the heading's own letterforms (outline → fill) instead of a decorative accent path, so this prop is ignored. Retained only for backwards-compatible type-checking.undefined
svgViewBoxstringOptional view box override for the svg-stroke-draw letter trace. Leave unset (recommended) to frame the text automatically from its measured glyph metrics; provide four numeric values with positive width and height to pin the framing.auto (measured)
svgAccessibleTitlestringOptional accessible title for the traced SVG. Leave unset for normal hero copy so assistive technology reads only the real HTML text; set it to expose the SVG as a labelled image.undefined
delay / duration / staggernumberTiming controls in seconds. Typewriter, scramble-decrypt, gradient-highlight, blur-focus, kinetic-emphasis-pop, and svg-stroke-draw use duration as bounded total reveal time. Scramble-decrypt caps updates so it cannot run indefinitely or exceed three updates per second. Gradient-highlight, blur-focus, kinetic-emphasis-pop, and svg-stroke-draw run once by default and use the shared repeat mechanism only when repeat is enabled. Scroll-responsive is tied to page scroll instead of duration and does not replay.0.05 / 0.48 / 0.045; typewriter duration 1.1; scramble duration 1.2; rotating keyword duration 0.34; gradient highlight duration 0.9; blur focus duration 0.42; kinetic emphasis duration 0.42; svg stroke draw duration 1.4; scroll responsive maps the first 220px of scroll
blur-focus content guidanceshort hero headingLimit blur-focus to short cinematic headings, ideally one line or under eight words. Do not combine the same heading with scale, parallax, or rotation; the component uses only small blur, opacity, and vertical translation, then finishes at blur(0px).Not set
kinetic-emphasis-pop content guidanceone or two important wordsUse kinetic emphasis for one or two words whose importance is also clear from static styling. Reduced motion removes scale animation and keeps the color, weight, and underline emphasis.Not set
scroll-responsive content guidancestorytelling hero headingUse scroll-responsive only when the complete heading is readable before scroll. The decorative layer maps the first 220px of page scroll to at most -32px of vertical movement and 92% opacity; reduced motion removes the scroll listener and transform style.Not set
svg-stroke-draw content guidanceshort single- or multi-line headingsvg-stroke-draw renders the heading as scalable SVG letterforms that trace their outline and then fill (outline → fill). It does not auto-wrap or use text-balance, so keep copy short and add hard line breaks (\n) for multi-line headings. The real HTML text stays available to assistive technology via a visually hidden copy. Reduced motion renders the final filled letters immediately and skips drawing.Not set
HeroTextAnimationProviderreducedMotion?: "user" | "always" | "never"Wraps examples or app surfaces with MotionConfig and a testable reduced-motion override."user"