# Hero

> Marketing hero with a pulsing announcement pill, a word-by-word headline with a volt-marked word, two CTAs, a copyable install command and a floating product shot of animated UI cards.

- Collection: Blocks
- Tier: free
- Docs: https://geturui.com/docs/hero-section
- Install: `npx shadcn@latest add @geturui/hero-section`
- Dependencies: motion, lucide-react

## Usage

```tsx
import { HeroSection } from "@/components/blocks/hero-section";

export default function Page() {
  return (
    <HeroSection
      variant="split"
      eyebrow="v2 is out: 40 new blocks"
      eyebrowHref="/changelog"
      headline="Ship interfaces your users will feel"
      mark="feel"
      primaryAction={{ label: "Get started", href: "/docs" }}
      secondaryAction={{ label: "View on GitHub", href: "https://github.com/acme/ui" }}
      installCommand="npx shadcn@latest add @acme/hero"
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| variant | `"centered" | "split"` | "centered" | Stack copy over the product shot, or place them side by side on wide containers. |
| eyebrow | `string | null` | "New: 14 page-section blocks" | Announcement pill text. null hides the pill. |
| eyebrowBadge | `string` | "New" | Label on the pulsing badge inside the pill. |
| eyebrowHref | `string` | — | Turns the pill into a link. |
| headline | `string` | — | Revealed word by word on mount. |
| mark | `string` | — | One word of the headline to underline with a volt swash. |
| subcopy | `string` | — | Supporting paragraph under the headline. |
| primaryAction | `{ label: string; href?: string; onClick?: () => void }` | — | Volt button with a nudging arrow. Renders a link when href is set. |
| secondaryAction | `{ label: string; href?: string; onClick?: () => void }` | — | Secondary button beside the primary. |
| installCommand | `string | null` | — | Shell command shown with a copy button. null hides the row. |
| showProductShot | `boolean` | true | Floating product shot built from animated divs. |
| className | `string` | — | Classes for the outer section. |

## Accessibility

- The headline keeps its full text in aria-label while the animated words are aria-hidden.
- The product shot is decorative and aria-hidden; its loops stop under reduced motion.
- CTAs render real links or buttons; the copy button announces Copied after use.

## Source

### components/blocks/hero-section.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArrowRight, Check, GitBranch, Search, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button, buttonClasses, type ButtonVariant } from "@/components/ui/button";
import { CopyButton } from "@/components/ui/copy-button";
import { NumberTicker } from "@/components/ui/number-ticker";
import { RevealText } from "@/components/ui/text-animation";

export interface HeroAction {
  label: string;
  /** Renders an anchor when set, otherwise a button. */
  href?: string;
  onClick?: () => void;
}

export interface HeroSectionProps {
  /** "centered" stacks copy over the product shot; "split" puts them side by side on large screens. */
  variant?: "centered" | "split";
  /** Text inside the announcement pill. Pass null to hide the pill. */
  eyebrow?: string | null;
  /** Short label on the pulsing badge inside the pill. */
  eyebrowBadge?: string;
  eyebrowHref?: string;
  headline?: string;
  /** One word of the headline to underline with a volt swash. */
  mark?: string;
  subcopy?: string;
  primaryAction?: HeroAction;
  secondaryAction?: HeroAction;
  /** Shell command shown with a copy button. Pass null to hide the row. */
  installCommand?: string | null;
  /** Show the floating product shot made of animated UI cards. */
  showProductShot?: boolean;
  className?: string;
}

function Cta({ action, variant }: { action: HeroAction; variant: ButtonVariant }) {
  const arrow = variant === "primary";
  if (action.href) {
    return (
      <a href={action.href} onClick={action.onClick} className={buttonClasses({ variant, size: "lg" })}>
        <span className="inline-flex items-center gap-[inherit]">
          {action.label}
          {arrow && (
            <ArrowRight aria-hidden className="size-4 transition-transform duration-300 ease-[var(--ease-spring)] group-hover/btn:translate-x-[3px]" />
          )}
        </span>
      </a>
    );
  }
  return (
    <Button variant={variant} size="lg" trailing={arrow ? "arrow" : undefined} onClick={action.onClick}>
      {action.label}
    </Button>
  );
}

/** Entrance + idle float for a product-shot card. */
function Floating({
  children,
  className,
  delay = 0,
  drift = 8,
  reduce,
}: {
  children: React.ReactNode;
  className?: string;
  delay?: number;
  drift?: number;
  reduce: boolean;
}) {
  return (
    <motion.div
      className={className}
      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 24, scale: 0.96, filter: "blur(6px)" }}
      animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
      transition={{ type: "spring", stiffness: 260, damping: 30, delay }}
    >
      <motion.div
        animate={reduce ? undefined : { y: [0, -drift, 0] }}
        transition={{ duration: 6 + delay * 2, repeat: Infinity, ease: "easeInOut", delay: delay + 0.6 }}
      >
        {children}
      </motion.div>
    </motion.div>
  );
}

function MiniSwitch({ reduce }: { reduce: boolean }) {
  return (
    <span aria-hidden className="relative flex h-5 w-9 items-center rounded-full border border-border-strong bg-surface-sunken p-[2px]">
      <motion.span
        className="absolute inset-0 rounded-full bg-volt"
        animate={reduce ? { opacity: 1 } : { opacity: [0, 0, 1, 1, 0] }}
        transition={{ duration: 4, repeat: Infinity, times: [0, 0.3, 0.4, 0.8, 0.9] }}
      />
      <motion.span
        className="relative block size-[14px] rounded-full bg-ink"
        animate={reduce ? { x: 16 } : { x: [0, 0, 16, 16, 0] }}
        transition={{ duration: 4, repeat: Infinity, times: [0, 0.3, 0.4, 0.8, 0.9], ease: "easeInOut" }}
      />
    </span>
  );
}

function ProductShot({ reduce, className }: { reduce: boolean; className?: string }) {
  return (
    <div aria-hidden className={cn("relative mx-auto w-full max-w-[640px] select-none", className)}>
      <div className="pointer-events-none absolute -inset-10 -z-10 rounded-full bg-volt-soft opacity-60 blur-3xl" />
      <Floating reduce={reduce} drift={4} className="relative">
        <div className="overflow-hidden rounded-lg border border-border bg-surface shadow-lg">
          <div className="flex items-center gap-2 border-b border-border px-4 py-3">
            <span className="size-2.5 rounded-full bg-surface-hover ring-1 ring-border" />
            <span className="size-2.5 rounded-full bg-surface-hover ring-1 ring-border" />
            <span className="size-2.5 rounded-full bg-surface-hover ring-1 ring-border" />
            <span className="ml-3 inline-flex h-6 flex-1 items-center gap-1.5 rounded-full bg-surface-sunken px-3 font-mono text-[11px] text-ink-subtle">
              <Search className="size-3" /> geturui.com/components
            </span>
          </div>
          <div className="grid grid-cols-[120px_1fr] gap-0 @xl:grid-cols-[150px_1fr]">
            <div className="hidden flex-col gap-2 border-r border-border p-4 @md:flex">
              {[64, 80, 52, 72, 60, 44].map((w, i) => (
                <span key={i} className={cn("h-2 rounded-full", i === 1 ? "bg-volt" : "bg-surface-raised")} style={{ width: `${w}%` }} />
              ))}
            </div>
            <div className="col-span-2 grid gap-3 p-4 @md:col-span-1">
              <div className="flex items-center justify-between">
                <span className="h-3 w-28 rounded-full bg-surface-raised" />
                <span className="inline-flex h-6 items-center rounded-full bg-volt px-3 text-[11px] font-semibold text-on-volt">Install</span>
              </div>
              <div className="grid grid-cols-2 gap-3">
                <div className="grid gap-2 rounded-md border border-border bg-bg p-3">
                  <span className="text-[11px] font-medium text-ink-muted">Reduced motion</span>
                  <MiniSwitch reduce={reduce} />
                </div>
                <div className="grid gap-2 rounded-md border border-border bg-bg p-3">
                  <span className="text-[11px] font-medium text-ink-muted">Build</span>
                  <span className="h-1.5 overflow-hidden rounded-full bg-surface-sunken">
                    <motion.span
                      className="block h-full origin-left rounded-full bg-volt"
                      animate={reduce ? { scaleX: 0.8 } : { scaleX: [0.1, 1, 1] }}
                      transition={{ duration: 3.2, repeat: Infinity, times: [0, 0.75, 1], ease: "easeInOut" }}
                    />
                  </span>
                </div>
              </div>
              <div className="grid gap-1.5 rounded-md bg-surface-sunken p-3 font-mono text-[11px] leading-4">
                <span>
                  <span className="text-ember-ink">import</span> <span className="text-ink">{"{ Button }"}</span>{" "}
                  <span className="text-ember-ink">from</span> <span className="text-volt-ink">&quot;@/ui/button&quot;</span>
                </span>
                <span className="text-ink-subtle">{"// spring press, nudging arrow"}</span>
                <span className="text-ink">
                  {"<Button trailing=\"arrow\">Ship it</Button>"}
                  {!reduce && (
                    <motion.span
                      className="ml-0.5 inline-block h-3 w-[6px] translate-y-0.5 bg-volt"
                      animate={{ opacity: [1, 0, 1] }}
                      transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
                    />
                  )}
                </span>
              </div>
            </div>
          </div>
        </div>
      </Floating>

      <Floating reduce={reduce} delay={0.25} className="absolute -left-6 top-20 hidden w-56 @xl:block">
        <div className="flex items-start gap-3 rounded-md bg-surface-raised p-3 shadow-md">
          <span className="grid size-7 shrink-0 place-items-center rounded-full bg-volt-soft text-volt-ink">
            <Check className="size-3.5" strokeWidth={2.5} />
          </span>
          <div className="min-w-0">
            <div className="text-[13px] font-semibold text-ink">Deployed to production</div>
            <div className="text-[11.5px] text-ink-muted">main · 41s ago</div>
          </div>
        </div>
      </Floating>

      <Floating reduce={reduce} delay={0.4} drift={10} className="absolute -right-6 -top-6 hidden w-44 @xl:block">
        <div className="grid gap-1 rounded-md bg-surface-raised p-3 shadow-md">
          <span className="text-[11px] font-medium text-ink-muted">Components shipped</span>
          <NumberTicker value={214} className="font-display text-2xl font-semibold text-ink" />
          <span className="inline-flex items-center gap-1 text-[11px] font-medium text-success">
            <Sparkles className="size-3" /> 12 this week
          </span>
        </div>
      </Floating>

      <Floating reduce={reduce} delay={0.55} drift={6} className="absolute -bottom-6 right-10 hidden @xl:block">
        <div className="inline-flex items-center gap-2 rounded-full border border-border bg-glass px-3 py-2 text-[12px] font-medium text-ink shadow-md glass">
          <GitBranch className="size-3.5 text-volt-ink" /> feat/hero-section
          <span className="rounded-full bg-success/15 px-1.5 text-[10px] font-semibold uppercase text-success">Merged</span>
        </div>
      </Floating>
    </div>
  );
}

/** Marketing hero: pulsing announcement pill, word-by-word headline with a marked word, two CTAs, a copyable install command and a floating product shot. */
export function HeroSection({
  variant = "centered",
  eyebrow = "New: 14 page-section blocks",
  eyebrowBadge = "New",
  eyebrowHref,
  headline = "Animated components your users will feel",
  mark = "feel",
  subcopy = "Copy-paste React components with spring motion, real keyboard support and tokens that match your brand. Built on Tailwind and motion.",
  primaryAction = { label: "Browse components" },
  secondaryAction = { label: "Read the docs" },
  installCommand = "npx shadcn@latest add @geturui/hero-section",
  showProductShot = true,
  className,
}: HeroSectionProps) {
  const reduce = useReducedMotion() ?? false;
  const split = variant === "split";

  const pillInner = (
    <>
      <Badge tone="new" pulse>
        {eyebrowBadge}
      </Badge>
      <span className="truncate">{eyebrow}</span>
      <ArrowRight aria-hidden className="size-3.5 shrink-0 text-ink-subtle transition-transform duration-300 ease-[var(--ease-spring)] group-hover/pill:translate-x-0.5" />
    </>
  );
  const pillClass =
    "group/pill inline-flex max-w-full items-center gap-2 rounded-full border border-border bg-surface py-1 pl-1 pr-3 text-[13px] font-medium text-ink-muted outline-none transition-colors hover:border-border-strong hover:text-ink focus-visible:shadow-[var(--focus-ring)]";

  return (
    <section className={cn("@container relative w-full overflow-hidden px-4 py-16 sm:px-6 sm:py-24", className)}>
      <div
        className={cn(
          "mx-auto grid max-w-6xl gap-14",
          split ? "items-center @4xl:grid-cols-[1.05fr_1fr] @4xl:gap-10" : "justify-items-center text-center",
        )}
      >
        <div className={cn("flex flex-col gap-6", split ? "items-start" : "items-center")}>
          {eyebrow &&
            (eyebrowHref ? (
              <motion.a
                href={eyebrowHref}
                className={pillClass}
                initial={{ opacity: 0, y: 8 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ type: "spring", stiffness: 300, damping: 30 }}
              >
                {pillInner}
              </motion.a>
            ) : (
              <motion.span
                className={pillClass}
                initial={{ opacity: 0, y: 8 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ type: "spring", stiffness: 300, damping: 30 }}
              >
                {pillInner}
              </motion.span>
            ))}

          <RevealText
            as="h1"
            text={headline}
            mark={mark}
            delay={0.1}
            className={cn(
              "text-balance font-display text-[40px] font-semibold leading-[1.05] tracking-[-1.2px] text-ink @xl:text-[56px] @xl:tracking-[-1.8px]",
              split ? "max-w-xl" : "max-w-3xl @4xl:text-[68px] @4xl:tracking-[-2.2px]",
            )}
          />

          <motion.p
            className={cn("text-balance text-base leading-7 text-ink-muted @xl:text-lg", split ? "max-w-lg" : "max-w-xl")}
            initial={{ opacity: 0, y: 10, filter: "blur(6px)" }}
            animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.45 }}
          >
            {subcopy}
          </motion.p>

          <motion.div
            className={cn("flex flex-wrap items-center gap-3", split ? "justify-start" : "justify-center")}
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ type: "spring", stiffness: 300, damping: 30, delay: 0.55 }}
          >
            <Cta action={primaryAction} variant="primary" />
            <Cta action={secondaryAction} variant="secondary" />
          </motion.div>

          {installCommand && (
            <motion.div
              className="inline-flex h-10 max-w-full items-center gap-2 rounded-full border border-border bg-surface-sunken pl-4 pr-1 font-mono text-[12.5px] text-ink-muted"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              transition={{ duration: 0.4, delay: 0.7 }}
            >
              <span aria-hidden className="text-volt-ink">
                $
              </span>
              <code className="truncate">{installCommand}</code>
              <CopyButton value={installCommand} label="Copy install command" className="rounded-full" />
            </motion.div>
          )}
        </div>

        {showProductShot && <ProductShot reduce={reduce} className={split ? "@4xl:max-w-none" : "mt-4"} />}
      </div>
    </section>
  );
}


```
