Skip to content
Dethink Components

ComponentsHeroTextAnimation

HeroTextAnimation

Expressive typography for memorable first impressions. Explore fourteen complete hero sections with accessible, responsive text animation.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

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. Animations play as you scroll into view. Use Replay to watch again; reduced-motion preferences are respected. Expand a source panel to copy a recipe.

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

Connected workspace — orbit & focus

A softly lit split hero with a word-by-word focus reveal and a connected workspace illustration.

Live preview
OrbitA little less friction

Your ideas, connected

Less busywork. More possibility.

Bring your notes, conversations, and next big idea into one beautifully connected workspace.

Find your flow

One space. Room for everything.

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

import {
  Button,
  HeroTextAnimation,
  HeroTextAnimationProvider,
} from "@dethink/components";
import {
  ArrowUpRight,
  AudioLines,
  Command,
  Globe2,
  Sparkles,
} from "lucide-react";
import { motion, useReducedMotion } from "motion/react";

export function HeroTextAnimationOrbitHero() {
  const reducedMotion = useReducedMotion();
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="orbit-hero-heading"
        className="bg-background text-foreground @container/hero relative isolate overflow-hidden"
      >
        <div
          aria-hidden="true"
          className="bg-primary/10 pointer-events-none absolute inset-x-0 top-0 -z-10 h-96 rounded-full blur-3xl"
        />
        <header className="border-border flex items-center justify-between border-b px-6 py-5">
          <span className="inline-flex items-center gap-2 text-sm font-semibold">
            <Command className="text-primary size-5" aria-hidden="true" /> Orbit
          </span>
          <span className="text-muted-foreground font-mono text-[10px] tracking-widest uppercase">
            A little less friction
          </span>
        </header>
        <div className="grid items-center gap-8 px-6 py-14 @min-[800px]/hero:grid-cols-2 @min-[800px]/hero:px-12 @min-[800px]/hero:py-20">
          <div className="min-w-0">
            <p className="text-primary text-xs font-medium tracking-[0.18em] uppercase">
              Your ideas, connected
            </p>
            {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
            <HeroTextAnimation
              as="h2"
              id="orbit-hero-heading"
              animation="blur-focus"
              splitBy="word"
              trigger="in-view"
              reducedMotionStrategy="static"
              stagger={0.12}
              text="Less busywork. More possibility."
              className="font-heading mt-6 text-[clamp(2rem,5.5cqi,4rem)] leading-[1.08] font-semibold tracking-tight"
            />
            <p className="text-muted-foreground mt-6 max-w-md text-base leading-7">
              Bring your notes, conversations, and next big idea into one
              beautifully connected workspace.
            </p>
            <Button
              asChild
              size="lg"
              className="mt-8"
              rightIcon={<ArrowUpRight />}
            >
              <a href="#installation-heading">Find your flow</a>
            </Button>
            <p className="text-muted-foreground mt-5 text-xs">
              One space. Room for everything.
            </p>
          </div>
          <motion.div
            aria-hidden="true"
            initial={false}
            whileInView={
              reducedMotion ? {} : { rotate: [-12, 0], scale: [0.94, 1] }
            }
            viewport={{ once: true, amount: 0.4 }}
            transition={{ duration: 1.8, ease: [0.22, 1, 0.36, 1] }}
            className="relative mx-auto aspect-square w-full max-w-96 motion-reduce:transform-none!"
          >
            <div className="border-primary/20 absolute inset-2 rounded-full border" />
            <div className="border-primary/15 absolute inset-12 rounded-full border border-dashed" />
            <div className="bg-primary/5 border-primary/20 absolute inset-24 rounded-full border" />
            <div className="absolute inset-0 grid place-items-center">
              <span className="bg-primary text-primary-foreground grid size-20 place-items-center rounded-3xl shadow-xl">
                <Command className="size-9" />
              </span>
            </div>
            {[
              {
                Icon: Sparkles,
                label: "Ideas",
                position: "top-4 left-1/2 -translate-x-1/2",
              },
              {
                Icon: AudioLines,
                label: "Conversations",
                position: "bottom-16 left-0",
              },
              {
                Icon: Globe2,
                label: "Your world",
                position: "right-0 top-1/2",
              },
            ].map(({ Icon, label, position }) => (
              <div
                key={label}
                className={`bg-background border-border absolute flex items-center gap-2 rounded-xl border p-3 shadow-sm ${position}`}
              >
                <Icon className="text-primary size-4" />
                <span className="text-xs font-medium">{label}</span>
              </div>
            ))}
          </motion.div>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

Objects & spaces — editorial curtain

Oversized serif lines, architectural forms, and a measured curtain reveal for a considered studio identity.

Live preview
Form & Feeling®Independent by design

Objects, spaces, and the moments between.

Make space for the unexpected.

Study No. 04 — Soft structure2026

Considered forms. Unexpected details. Everyday things that invite you to look a little closer.

Thoughtfully made, slowly enjoyed.Edition 004 / An ongoing practice
Show sourceexamples/hero-text-animation/editorial-hero.tsx
examples/hero-text-animation/editorial-hero.tsx
"use client";

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

export function HeroTextAnimationEditorialHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="editorial-hero-heading"
        className="bg-muted/30 text-foreground @container/hero overflow-hidden"
      >
        <header className="border-border flex justify-between gap-4 border-b px-6 py-5 font-mono text-[10px] tracking-[0.18em] uppercase">
          <span>Form &amp; Feeling®</span>
          <span>Independent by design</span>
        </header>
        <div className="grid gap-10 px-6 py-14 @min-[800px]/hero:grid-cols-[1.3fr_1fr] @min-[800px]/hero:px-12 @min-[800px]/hero:py-20">
          <div className="min-w-0">
            <p className="text-muted-foreground text-xs">
              Objects, spaces, and the moments between.
            </p>
            {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
            <HeroTextAnimation
              as="h2"
              id="editorial-hero-heading"
              animation="masked-curtain"
              trigger="in-view"
              reducedMotionStrategy="static"
              stagger={0.18}
              text={"Make space\nfor the\nunexpected."}
              className="mt-8 font-serif text-[clamp(2rem,6cqi,4.5rem)] leading-[1.12] tracking-tight"
            />
            <div className="mt-9">
              <Button asChild variant="outline" rightIcon={<ArrowUpRight />}>
                <a href="#installation-heading">Explore the collection</a>
              </Button>
            </div>
          </div>
          <div className="relative min-w-0">
            <div
              aria-hidden="true"
              className="bg-primary/10 relative flex aspect-[4/5] max-h-[420px] items-end justify-center overflow-hidden rounded-t-full"
            >
              <div className="bg-primary/15 absolute inset-x-8 bottom-0 h-3/4 rounded-t-full" />
              <div className="bg-primary/30 absolute bottom-0 h-2/3 w-2/3 rounded-t-full" />
              <div className="bg-background absolute bottom-0 h-1/2 w-1/3 rounded-t-full shadow-2xl" />
            </div>
            <span
              aria-hidden="true"
              className="text-primary pointer-events-none absolute top-8 right-8"
            >
              <ArrowDownRight className="size-10" />
            </span>
            <div className="border-border mt-5 flex justify-between gap-3 border-t pt-4 text-xs">
              <span>Study No. 04 — Soft structure</span>
              <span className="text-muted-foreground">2026</span>
            </div>
            <p className="text-muted-foreground mt-4 max-w-sm text-sm leading-6">
              Considered forms. Unexpected details. Everyday things that invite
              you to look a little closer.
            </p>
          </div>
        </div>
        <footer className="border-border flex flex-wrap justify-between gap-3 border-t px-6 py-5 text-[10px] tracking-[0.16em] uppercase">
          <span>Thoughtfully made, slowly enjoyed.</span>
          <span className="text-muted-foreground">
            Edition 004 / An ongoing practice
          </span>
        </footer>
      </section>
    </HeroTextAnimationProvider>
  );
}

Growth platform — signal & motion

A crisp product introduction with kinetic emphasis and a staggered chart entrance.

Live preview
Clarity for your next chapter

Big ambition. Clear signal.

Every metric tells a story. See what is working, find your next opportunity, and move forward with confidence.

Growth overviewIllustrative data · Last 12 months

Annual recurring revenue

$128,400

32.8% this year

JANDEC
Show sourceexamples/hero-text-animation/analytics-hero.tsx
examples/hero-text-animation/analytics-hero.tsx
"use client";

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

const bars = [28, 42, 35, 56, 48, 63, 54, 76, 68, 86, 79, 98];

export function HeroTextAnimationAnalyticsHero() {
  const reducedMotion = useReducedMotion();
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="analytics-hero-heading"
        className="bg-background text-foreground @container/hero relative overflow-hidden px-6 pt-12 @min-[600px]/hero:px-12 @min-[600px]/hero:pt-16"
      >
        <div className="relative z-10 mx-auto max-w-2xl text-center">
          <span className="border-border bg-muted/40 inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs">
            <ChartNoAxesCombined
              aria-hidden="true"
              className="text-primary size-4"
            />{" "}
            Clarity for your next chapter
          </span>
          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            id="analytics-hero-heading"
            animation="kinetic-emphasis-pop"
            emphasisWords={["signal."]}
            trigger="in-view"
            reducedMotionStrategy="static"
            text="Big ambition. Clear signal."
            className="font-heading mt-7 text-[clamp(2rem,6cqi,4rem)] leading-[1.1] font-semibold tracking-tight"
          />
          <p className="text-muted-foreground mx-auto mt-6 max-w-lg text-base leading-7">
            Every metric tells a story. See what is working, find your next
            opportunity, and move forward with confidence.
          </p>
          <div className="mt-7 flex flex-wrap justify-center gap-3">
            <Button asChild rightIcon={<ArrowRight />}>
              <a href="#installation-heading">Build your dashboard</a>
            </Button>
            <Button asChild variant="outline">
              <a href="#props-heading">Explore the API</a>
            </Button>
          </div>
        </div>
        <div className="border-border bg-muted/20 relative mx-auto mt-12 max-w-3xl rounded-t-2xl border border-b-0 p-4 shadow-xl @min-[600px]/hero:p-7">
          <div className="border-border flex flex-wrap items-center justify-between gap-3 border-b pb-5">
            <span className="text-sm font-medium">Growth overview</span>
            <span className="text-muted-foreground text-xs">
              Illustrative data · Last 12 months
            </span>
          </div>
          <div className="grid gap-6 pt-6 @min-[600px]/hero:grid-cols-[1fr_2fr]">
            <div>
              <p className="text-muted-foreground text-xs">
                Annual recurring revenue
              </p>
              <p className="mt-2 text-4xl font-semibold tracking-tight">
                $128,400
              </p>
              <p className="text-primary mt-3 inline-flex items-center gap-1 text-xs">
                <ArrowUpRight aria-hidden="true" className="size-3.5" /> 32.8%
                this year
              </p>
            </div>
            <div
              role="img"
              aria-label="Illustrative monthly growth chart trending upward over twelve months"
              className="flex h-40 items-end gap-2"
            >
              {bars.map((height, index) => (
                <motion.div
                  key={index}
                  initial={false}
                  whileInView={
                    reducedMotion
                      ? {}
                      : { scaleY: [0.05, 1], opacity: [0.4, 1] }
                  }
                  viewport={{ once: true, amount: 0.2 }}
                  transition={{
                    duration: 0.7,
                    delay: index * 0.045,
                    ease: [0.22, 1, 0.36, 1],
                  }}
                  style={{ height: `${height}%` }}
                  className="bg-primary/65 last:bg-primary min-w-0 flex-1 origin-bottom rounded-t motion-reduce:transform-none! motion-reduce:opacity-100!"
                />
              ))}
            </div>
          </div>
          <div className="text-muted-foreground border-border mt-4 flex justify-between border-t border-dashed pt-3 font-mono text-[10px]">
            <span>JAN</span>
            <span>DEC</span>
          </div>
        </div>
      </section>
    </HeroTextAnimationProvider>
  );
}

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.

Live preview
Hero section recipe

Your next big idea starts here.

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-heading", label: "Examples" },
  { href: "#installation-heading", label: "Install" },
  { href: "#props-heading", 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 @container/hero rounded-md border"
      >
        <div className="mx-auto max-w-5xl px-5 py-5 @min-[480px]/hero:px-8 @min-[900px]/hero: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-heading"
              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 @min-[480px]/hero:inline">
                Dethink Hero
              </span>
            </a>

            <nav
              aria-label="Hero recipe navigation"
              className="hidden min-w-0 flex-1 @min-[680px]/hero: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"
                >
                  <span className="inline-flex items-center gap-1.5">
                    Resources
                    <ChevronDown aria-hidden="true" className="size-3.5" />
                  </span>
                </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 @min-[480px]/hero: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 @min-[480px]/hero:px-8 @min-[900px]/hero:grid-cols-[minmax(0,1fr)_22rem] @min-[900px]/hero:items-center @min-[900px]/hero:px-10 @min-[900px]/hero:pt-10 @min-[900px]/hero: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">
              {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
              <HeroTextAnimation
                as="h2"
                trigger="in-view"
                reducedMotionStrategy="static"
                id="hero-text-production-heading"
                text="Your next big idea starts here."
                className="font-heading text-foreground max-w-3xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.03] font-semibold tracking-normal"
              />
              <p className="text-muted-foreground max-w-xl text-base leading-7 @min-[480px]/hero: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 flex-wrap gap-3 @min-[480px]/hero:flex-row">
              <Button asChild size="lg" rightIcon={<ArrowRight />}>
                <a href="#installation-heading">Install component</a>
              </Button>
              <Button asChild size="lg" variant="outline">
                <a href="#props-heading">Review props</a>
              </Button>
            </div>
          </div>

          <ul className="grid min-w-0 gap-3 @min-[480px]/hero:grid-cols-2 @min-[900px]/hero: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.

Live preview
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 @container/hero overflow-hidden rounded-md border"
      >
        <header className="border-border flex items-center justify-between gap-4 border-b px-5 py-4 @min-[480px]/hero:px-8 @min-[900px]/hero:px-12">
          <a
            href="#examples-heading"
            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 @min-[680px]/hero:flex"
          >
            {navItems.map((item) => (
              <a
                key={item}
                href="#examples-heading"
                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 @min-[480px]/hero:inline">
            Est. MMXXVI
          </span>
        </header>

        <div className="mx-auto max-w-3xl px-5 py-16 text-center @min-[480px]/hero:px-8 @min-[900px]/hero: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"
          />
          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            animation="masked-curtain"
            ariaLabel="We design brands that move with intent."
            id="hero-text-masked-curtain-heading"
            text={"We design brands\nthat move with intent."}
            className="font-heading text-foreground mx-auto mt-8 max-w-3xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.05] font-semibold tracking-tight text-balance"
          />
          <p className="text-muted-foreground mx-auto mt-7 max-w-xl text-base leading-7 @min-[480px]/hero: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 @min-[480px]/hero:flex-row">
            <Button asChild size="lg" variant="outline">
              <a href="#installation-heading">Start a project</a>
            </Button>
            <a
              href="#examples-heading"
              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 @min-[480px]/hero:grid-cols-3">
          {disciplines.map(({ index, title, description }, position) => (
            <li
              key={title}
              className={`px-5 py-8 @min-[480px]/hero:px-8 @min-[900px]/hero:px-12 ${
                position > 0
                  ? "border-border border-t @min-[480px]/hero:border-t-0 @min-[480px]/hero: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.

Live preview
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 @container/hero overflow-hidden rounded-md border"
      >
        <div className="mx-auto grid max-w-5xl items-center gap-10 px-5 py-12 @min-[480px]/hero:px-8 @min-[900px]/hero:grid-cols-[minmax(0,1fr)_26rem] @min-[900px]/hero:px-10 @min-[900px]/hero: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>

            {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
            <HeroTextAnimation
              as="h2"
              trigger="in-view"
              reducedMotionStrategy="static"
              animation="typewriter"
              duration={1.9}
              id="hero-text-typewriter-heading"
              text="Ship your CLI in an afternoon."
              className="font-heading text-foreground mt-6 max-w-xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.05] font-semibold tracking-tight"
            />
            <p className="text-muted-foreground mt-6 max-w-md text-base leading-7 @min-[480px]/hero: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 flex-wrap gap-3 @min-[480px]/hero:flex-row @min-[480px]/hero:items-center">
              <Button asChild size="lg" leftIcon={<BookOpen />}>
                <a href="#installation-heading">Read the docs</a>
              </Button>
              <Button
                asChild
                size="lg"
                variant="ghost"
                leftIcon={<GitBranch />}
                rightIcon={<Star />}
              >
                <a href="#examples-heading">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"
                />
              </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.

Live preview
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 @container/hero 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 @min-[480px]/hero:px-8 @min-[900px]/hero:grid-cols-[minmax(0,1fr)_22rem] @min-[900px]/hero:px-10 @min-[900px]/hero: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>

            {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
            <HeroTextAnimation
              as="h2"
              trigger="in-view"
              reducedMotionStrategy="static"
              animation="scramble-decrypt"
              duration={1}
              id="hero-text-scramble-heading"
              text="Decrypt threats before they move."
              className="font-heading text-foreground mt-6 max-w-2xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.05] font-semibold tracking-tight"
            />
            <p className="text-muted-foreground mt-6 max-w-lg text-base leading-7 @min-[480px]/hero: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 flex-wrap gap-3 @min-[480px]/hero:flex-row">
              <Button asChild size="lg" rightIcon={<ArrowRight />}>
                <a href="#installation-heading">Request access</a>
              </Button>
              <Button asChild size="lg" variant="outline">
                <a href="#props-heading">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 rounded-full" />
                  <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.

Live preview
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 {
  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 teamAvatars = [
  { initials: "SO", tone: "bg-primary/15 text-foreground" },
  { initials: "CS", tone: "bg-info/15 text-foreground" },
  { initials: "FL", tone: "bg-success/15 text-foreground" },
  { initials: "PM", tone: "bg-warning/20 text-foreground" },
  { initials: "OP", tone: "bg-muted text-muted-foreground" },
];

export function HeroTextAnimationRotatingKeywordHero() {
  return (
    <HeroTextAnimationProvider>
      <section
        aria-labelledby="hero-text-rotating-keyword-heading"
        className="bg-background text-foreground border-border @container/hero overflow-hidden rounded-md border"
      >
        <div className="mx-auto max-w-3xl px-5 py-16 text-center @min-[480px]/hero:px-8 @min-[900px]/hero: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>

          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            animation="rotating-keyword"
            ariaLabel="Launch pages for startups, agencies, product teams, and founders."
            duration={0.34}
            id="hero-text-rotating-keyword-heading"
            autoRotateKeywords
            rotatingKeywordInterval={1.4}
            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-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.06] font-semibold tracking-tight text-balance [&_[data-slot=hero-text-animation-motion]]:justify-center"
          />

          <p className="text-muted-foreground mx-auto mt-6 max-w-xl text-base leading-7 @min-[480px]/hero: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 @min-[480px]/hero:flex-row">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation-heading">Start building free</a>
            </Button>
            <Button asChild size="lg" variant="ghost" leftIcon={<Play />}>
              <a href="#examples-heading">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.

Live preview
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 @container/hero 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 @min-[480px]/hero:px-8 @min-[900px]/hero: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>

          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            animation="gradient-highlight"
            duration={1.8}
            id="hero-text-gradient-highlight-heading"
            text="Write copy that sounds unmistakably you."
            className="font-heading text-foreground mx-auto mt-7 max-w-2xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.05] font-semibold tracking-tight text-balance"
          />

          <p className="text-muted-foreground mx-auto mt-6 max-w-xl text-base leading-7 @min-[480px]/hero: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 @min-[480px]/hero: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.

Live preview

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 @container/hero 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 @min-[480px]/hero:px-8 @min-[900px]/hero: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>

          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            animation="blur-focus"
            duration={1.8}
            id="hero-text-blur-focus-heading"
            text="Designed to disappear into your day."
            className="font-heading text-foreground mt-9 max-w-2xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.05] font-semibold tracking-tight text-balance"
          />

          <p className="text-muted-foreground mt-7 max-w-md text-base leading-7 @min-[480px]/hero: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-heading">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 @min-[480px]/hero:flex-row @min-[480px]/hero:px-8 @min-[900px]/hero:px-12">
          <span className="text-muted-foreground tracking-wide">
            Available Fall 2026
          </span>
          <a
            href="#installation-heading"
            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.

Live preview

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 @container/hero 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 @min-[480px]/hero:px-8 @min-[900px]/hero: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>

          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            animation="blur-focus"
            splitBy="word"
            duration={0.7}
            stagger={0.09}
            id="hero-text-word-blur-focus-heading"
            text="Every frame pulled into perfect focus."
            className="font-heading text-foreground mt-8 max-w-2xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.05] font-semibold tracking-tight text-balance"
          />

          <p className="text-muted-foreground mt-7 max-w-lg text-base leading-7 @min-[480px]/hero: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 flex-wrap gap-3 @min-[480px]/hero:flex-row @min-[480px]/hero:items-center">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation-heading">Book a session</a>
            </Button>
            <a
              href="#examples-heading"
              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 @min-[480px]/hero: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.

Live preview
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 @container/hero overflow-hidden rounded-md border"
      >
        <div className="mx-auto max-w-3xl px-5 py-16 text-center @min-[480px]/hero:px-8 @min-[900px]/hero: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>

          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            animation="kinetic-emphasis-pop"
            duration={0.5}
            stagger={0.07}
            emphasisWords={["revenue", "faster"]}
            id="hero-text-kinetic-emphasis-heading"
            text="Turn more visitors into revenue, faster."
            className="font-heading text-foreground mx-auto mt-6 max-w-2xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.06] font-semibold tracking-tight text-balance"
          />

          <p className="text-muted-foreground mx-auto mt-6 max-w-xl text-base leading-7 @min-[480px]/hero: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 @min-[480px]/hero:flex-row">
            <Button asChild size="lg" rightIcon={<ArrowRight />}>
              <a href="#installation-heading">Start free trial</a>
            </Button>
            <Button asChild size="lg" variant="outline">
              <a href="#examples-heading">Book a demo</a>
            </Button>
          </div>
        </div>

        <dl className="border-border grid border-t @min-[480px]/hero:grid-cols-3">
          {metrics.map(({ value, label }, position) => (
            <div
              key={label}
              className={`px-5 py-8 text-center @min-[480px]/hero:px-8 ${
                position > 0
                  ? "border-border border-t @min-[480px]/hero:border-t-0 @min-[480px]/hero:border-l"
                  : ""
              }`}
            >
              <dt className="text-primary font-heading inline-flex items-center gap-1.5 text-3xl font-semibold tracking-tight @min-[480px]/hero: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.

Live preview
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 @container/hero overflow-hidden rounded-md border"
      >
        <div className="border-border text-muted-foreground grid grid-cols-2 border-b text-xs font-medium tracking-[0.22em] uppercase @min-[480px]/hero:grid-cols-3">
          <span className="px-5 py-3.5 @min-[480px]/hero:px-8">Dethink</span>
          <span className="border-border hidden border-x px-5 py-3.5 text-center @min-[480px]/hero:block">
            Campaign · 2026
          </span>
          <span className="px-5 py-3.5 text-right @min-[480px]/hero:px-8">
            № 01
          </span>
        </div>

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

          {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
          <HeroTextAnimation
            as="h2"
            trigger="in-view"
            reducedMotionStrategy="static"
            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 @min-[480px]/hero: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-heading">Explore the identity</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 @min-[480px]/hero: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.

Live previewScroll to animate ↕

A field guide · Chapter 01

Every product starts as a quiet, stubborn idea.

Scroll down to gently shrink and lift the headline; scroll back up to bring it forward. Nothing essential moves out of reach — the heading stays readable the whole way down.

Scroll down and back up

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 @container/hero overflow-hidden rounded-md border"
      >
        <div className="mx-auto grid max-w-5xl gap-10 px-5 py-16 @min-[480px]/hero:px-8 @min-[900px]/hero:grid-cols-[minmax(0,1fr)_16rem] @min-[900px]/hero:px-10 @min-[900px]/hero: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>

            {/* eslint-disable-next-line jsx-a11y/heading-has-content -- The component renders its text prop as accessible heading content. */}
            <HeroTextAnimation
              as="h2"
              trigger="in-view"
              reducedMotionStrategy="static"
              animation="scroll-responsive"
              id="hero-text-scroll-responsive-heading"
              text="Every product starts as a quiet, stubborn idea."
              className="font-heading text-foreground mt-16 max-w-2xl text-[clamp(1.875rem,5.5cqi,3.75rem)] leading-[1.06] font-semibold tracking-tight text-balance"
            />

            <p className="text-muted-foreground mt-7 max-w-lg text-base leading-7 @min-[480px]/hero:text-lg">
              Scroll down to gently shrink and lift the headline; scroll back up
              to bring it forward. 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-heading">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" />
              Scroll down and back up
            </p>
          </div>

          <nav
            aria-label="Chapters"
            className="border-border @min-[900px]/hero:border-l @min-[900px]/hero: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 @min-[480px]/hero:px-8 @min-[900px]/hero: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 spans 55% of viewport height (minimum 220px) after the heading reaches 70% of the viewport
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 begins when the heading reaches 70% of the viewport (or its initial position for a hero already above that point), then spans 55% of viewport height (minimum 220px) to reach -32px of vertical movement, 90% scale, 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"