Blocks

Hero Section

A props-driven landing hero — eyebrow, headline, dual CTAs, social-proof bar and an optional visual that collapses to a centred column when omitted.

Preview in your theme

Loading preview…

import * as React from "react"
import { Star } from "lucide-react"
import { cn } from "@/lib/utils"

/**
 * A CTA must actually go somewhere: either an href (a real id on this page or a real
 * route) or an onClick. The type makes a dead button with neither impossible.
 */
export type HeroAction =
  | { label: React.ReactNode; href: string; onClick?: React.MouseEventHandler<HTMLAnchorElement> }
  | { label: React.ReactNode; href?: undefined; onClick: React.MouseEventHandler<HTMLButtonElement> }

export interface HeroProofAvatar {
  /** Used for alt text and the initials fallback — required */

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/hero-section.json

Prompt

Build a React + TypeScript + Tailwind "HeroSection" block (lucide-react Star).

Contract
- Props (all optional except title): eyebrow, title, subtitle, primaryAction,
  secondaryAction, proof, visual, align?: "start" | "center",
  titleAs?: "h1" | "h2" (default "h1"), plus the native <section> props and
  className. Nothing is hardcoded in JSX — a buyer configures the block by
  passing values.
- HeroAction is a union that makes dead buttons unrepresentable:
  { label; href: string; onClick? } | { label; onClick: handler; href?: undefined }.
  An action must carry a real destination — an href (in-page "#id" that exists,
  or a route) or an onClick. Never emit href="#".
- proof?: { avatars?: { name; src? }[]; rating?: number; label?: ReactNode;
  logos?: ReactNode[] } — every part optional, render only what arrives.
- No hooks, no "use client": the block stays usable as a server component when
  the consumer only passes href actions.

Behavior
- Layout is derived, not configured twice: align defaults to "start" when a
  visual is passed and "center" when it isn't, so the block collapses into a
  centred single column instead of leaving an empty half. align overrides it.
- Responsive by CONTAINER, not viewport (@container/hero): the two-column split
  and the largest type step only switch on when the block's own slot is wide
  (@5xl), so the same component behaves in a narrow docs column, a sidebar
  preview and a full-bleed page. Type scale: text-4xl → @2xl:text-5xl →
  @5xl:text-6xl.
- Overflow discipline: the copy column is a flex column with items-start, so a
  child sized by fit-content can exceed its parent — an unbreakable long word
  (min-content is NOT reduced by break-words) then pushes the section past the
  viewport. Give the heading/subtitle w-full + break-words, and min-w-0 on the
  copy column, so long headlines wrap instead of shoving the CTAs off screen.
- CTAs render as <a> when href is present and <button type="button"> otherwise;
  both share one style base so the tone (primary/secondary) is the only diff.
- Proof bar: avatars overlap (-space-x-2, ring-background separators), capped at
  5 with a "+N" chip; a missing src falls back to initials, never a broken img.
  rating is clamped to 0..5 and dropped entirely when not finite; the star row
  is aria-hidden with an "out of 5" sr-only companion. logos render as a wrapped
  muted row.

Rendering & styling
- Semantic tokens only: bg-primary/text-primary-foreground for the primary CTA,
  border + hover:bg-primary/10 for the secondary (a tinted hover works on any
  surface, unlike hover:bg-accent over bg-muted), text-muted-foreground for
  supporting copy, bg-muted/60 eyebrow pill, bg-muted/40 visual frame.
- The visual is wrapped in one aspect-ratio frame (aspect-[4/3] →
  @2xl:aspect-[16/10], rounded-xl border, overflow-hidden) so any child — image,
  video, live component — keeps the row stable.
- focus-visible ring-2 ring-ring with ring-offset-background on both CTAs;
  cn() merges the consumer className onto the section.

Customization levers
- Density: px-6 py-16 / @2xl:py-20 and the gap-10 column gap set the vertical
  rhythm; max-w-6xl on the inner row and max-w-2xl on the copy column set
  measure.
- Which sub-blocks exist: omit eyebrow / subtitle / secondaryAction / proof /
  visual and the layout closes up — no placeholder holes.
- Split threshold: move @5xl/hero to @4xl/hero for an earlier two-column switch,
  or delete the split classes for an always-stacked hero.
- Proof emphasis: swap initials avatars for images via proof.avatars[].src, drop
  the rating for a logo-only trust row, or move the logo row below the fold.
- CTA shape: add w-full @sm/hero:w-auto to the CTA base for full-bleed mobile
  buttons; add a trailing lucide icon inside label (the base is a gap-2 flex).
- Motion: the block ships static; wrap the copy column in your motion library and
  guard it with motion-reduce:animate-none if you want an entrance.

Concepts

  • Derived layoutalign defaults from the presence of visual, so removing the screenshot re-centres the hero instead of leaving a blank column; passing align explicitly is the escape hatch.
  • Container-query responsiveness — the split and the largest type step react to the block's own inline size (@container/hero), not the viewport, so the same hero reads correctly in a narrow docs column and on a full-bleed page.
  • Actions that cannot be dead — the HeroAction union requires either an href or an onClick; there is no shape that renders a clickable-looking element with nothing behind it.
  • min-content ignores break-words — inside an items-start flex column a heading is sized by fit-content, and an unbreakable long word makes that larger than the parent; w-full re-clamps it so the headline wraps instead of pushing the CTAs out of the viewport.
  • Fallbacks over holes — a missing avatar src becomes initials, an out-of-range rating is clamped, extra avatars fold into +N; the proof bar degrades instead of rendering broken images or 9 stars.

On This Page