Skip to content
Facade UI
Colour mode
GitHub (opens in a new tab)
blocksection

Hero — centred

The classic centred landing hero: eyebrow, headline, copy, buttons, and optional media below.

Also ships a motion variant: Hero — centred (motion). The static one is the default.

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add https://facadeui.dev/r/hero-centered.json

Pulls in container, cta-group, eyebrow, heading, section, types, utils. The CLI installs them for you.

Usage

The exact source of the preview above.

demos/hero-centered.tsx
import { Badge } from "@registry/ui/badge"
import { HeroCentered } from "@registry/sections/hero-centered"

import { ACTIONS } from "./content"

export function Demo() {
  return (
    <HeroCentered
      banner={<Badge variant="outline">v0.1 is out</Badge>}
      eyebrow="Free and open source"
      title="The front of every great website"
      description="Marketing sections and a design system you install with the shadcn CLI. The source lands in your project — yours to read, change and own."
      actions={ACTIONS}
      note="MIT licensed. No account, no gating."
      headingLevel={1}
      spacing="md"
    />
  )
}

Source

What the CLI copies into your project, byte for byte.

components/sections/hero-centered.tsx
/**
 * HeroCentered — the classic centred landing hero: eyebrow, headline, copy,
 * buttons, and optional media below.
 *
 * a11y: `headingLevel` defaults to `1`, because a hero usually *is* the page
 * title — but it stays a prop, so the same component can be an `<h2>` further
 * down a page without its appearance changing. `size` controls the visual scale
 * independently.
 *
 * The band is a `<div>`, not a `<section>`: a hero at the top of a page is the
 * start of `<main>`, and wrapping it in a second landmark only adds noise to the
 * landmark list.
 *
 * Dependencies: react, @/lib/types, @/lib/utils,
 * @/components/ui/container, @/components/ui/cta-group, @/components/ui/eyebrow,
 * @/components/ui/heading, @/components/ui/section.
 */

import type { ElementType, ReactNode } from "react"

import type {
  CtaItem,
  HeadingLevel,
  LinkComponent,
  StackSlotProps,
} from "@/lib/types"
import { cn, slugId } from "@/lib/utils"
import { Container } from "@/components/ui/container"
import { CtaGroup } from "@/components/ui/cta-group"
import { Eyebrow } from "@/components/ui/eyebrow"
import { Heading } from "@/components/ui/heading"
import { Section } from "@/components/ui/section"

export interface HeroCenteredProps extends StackSlotProps {
  title: string
  description?: string
  eyebrow?: ReactNode
  actions?: CtaItem[]
  link?: LinkComponent
  /** Small print under the buttons — "No card required", and so on. */
  note?: ReactNode
  /** Rendered above the eyebrow. A Badge, an announcement pill, a rating. */
  banner?: ReactNode
  /** Rendered below the buttons. A screenshot, a video, a logo strip. */
  media?: ReactNode
  headingLevel?: HeadingLevel
  size?: "md" | "lg" | "xl"
  as?: "section" | "div"
  spacing?: "sm" | "md" | "lg" | "none"
  className?: string
  id?: string
}

const titleSizes = {
  md: "text-display-md",
  lg: "text-display-lg",
  xl: "text-display-xl",
} as const

export function HeroCentered({
  title,
  description,
  eyebrow,
  actions,
  link,
  note,
  banner,
  media,
  stackAs,
  blockAs,
  headingLevel = 1,
  size = "lg",
  as = "div",
  spacing = "lg",
  className,
  id,
}: HeroCenteredProps) {
  const Stack = (stackAs ?? "div") as ElementType
  const Block = (blockAs ?? "div") as ElementType
  const titleId = id ? `${id}-title` : slugId(title)

  return (
    <Section
      as={as}
      spacing={spacing}
      labelledBy={as === "section" ? titleId : undefined}
      id={id}
      className={className}
    >
      <Container className="flex flex-col items-center gap-12">
        <Stack className="flex max-w-3xl flex-col items-center gap-6 text-center">
          {banner ? <Block>{banner}</Block> : null}
          {eyebrow ? (
            <Block>
              <Eyebrow tone="primary">{eyebrow}</Eyebrow>
            </Block>
          ) : null}

          <Block>
            <Heading
              level={headingLevel}
              id={titleId}
              className={cn("text-balance font-semibold", titleSizes[size])}
            >
              {title}
            </Heading>
          </Block>

          {description ? (
            <Block>
              <p className="text-muted-foreground max-w-2xl text-pretty text-lg sm:text-xl">
                {description}
              </p>
            </Block>
          ) : null}

          {actions?.length ? (
            <Block className="w-full sm:w-auto">
              <CtaGroup
                items={actions}
                link={link}
                size="lg"
                align="center"
                stackOnMobile
              />
            </Block>
          ) : null}

          {note ? (
            <Block>
              <p className="text-muted-foreground text-pretty text-sm">{note}</p>
            </Block>
          ) : null}
        </Stack>

        {media ? <div className="w-full">{media}</div> : null}
      </Container>
    </Section>
  )
}

Props

HeroCenteredProps

Extends StackSlotProps.

Props for HeroCenteredProps
PropTypeDefault
title*Requiredstring
descriptionstring
eyebrowReactNode
actionsCtaItem[]
linkLinkComponent
noteSmall print under the buttons — "No card required", and so on.ReactNode
bannerRendered above the eyebrow. A Badge, an announcement pill, a rating.ReactNode
mediaRendered below the buttons. A screenshot, a video, a logo strip.ReactNode
headingLevelHeadingLevel1
size"md" | "lg" | "xl""lg"
as"section" | "div""div"
spacing"sm" | "md" | "lg" | "none""lg"
classNamestring
idstring