Skip to content
Caseroom

Blocks · Marketing

Hero

The opening section: h1, one sentence, two actions and an install command; compact for an inner page.

48% of the 2,062 sites in The UI Index have a hero (982 sites).

Open source

Ship the page, not the boilerplate

A starting point for your product site: clear type, two actions and an install command your readers can copy.

npx create-my-app@latest

Pricing

Plans for every stage

A smaller h1 and less air, for the head of an inner page.

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/hero.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/hero.json

Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the Caseroom theme. The HTML needs only the compiled CSS.

hero.classes.ts
export const hero = {
  // default: the opening of a home or product page. compact: the head of an inner page (pricing, blog, docs), a
  // smaller h1 and less air, so the content it introduces starts above the fold.
  root: { default: 'bg-background px-6 py-20 sm:py-28', compact: 'bg-background px-6 py-12 sm:py-16' },
  // center: centred from 640px up and left-aligned on phones, where a centred lede would run to five lines.
  // start: on the page's left edge at every width, for a page whose next sections are left-aligned (one axis).
  inner: {
    center: 'mx-auto flex max-w-3xl flex-col items-start text-left sm:items-center sm:text-center',
    start: 'flex max-w-3xl flex-col items-start text-left',
  },
  title: {
    default: 'mt-5 text-4xl leading-[1.05] font-semibold tracking-[-0.035em] text-balance sm:text-6xl',
    compact: 'mt-4 text-3xl leading-[1.1] font-semibold tracking-[-0.03em] text-balance sm:text-5xl',
  },
  description: {
    default: 'mt-5 max-w-xl text-base text-pretty text-muted-foreground sm:text-lg',
    compact: 'mt-4 max-w-xl text-base text-pretty text-muted-foreground',
  },
  actions: { center: 'flex flex-wrap gap-3 sm:justify-center', start: 'flex flex-wrap gap-3' },
  actionsGap: { default: 'mt-8', compact: 'mt-6' },
  // A long command wraps instead of pushing the page wider than a phone (320px).
  command: 'mt-6 inline-flex max-w-full items-start gap-2 rounded-md border bg-muted px-3 py-1.5 text-left font-mono text-[13px] text-muted-foreground [&_code]:min-w-0 [&_code]:[overflow-wrap:anywhere]',
};

/** A command split so it breaks only at spaces and after / or @ (not inside //), never inside a name (button.json). */
export const commandParts = (cmd: string) => cmd.split(' ').map((word) => word.split(/(?<=[/@])(?!\/)/));
export const partClasses = 'whitespace-nowrap';

export type HeroAction = { label: string; href: string };
export type HeroContent = {
  eyebrow?: string;
  title: string;
  description?: string;
  primary?: HeroAction;
  secondary?: HeroAction;
  /** An install command shown under the buttons, e.g. "npx shadcn add @caseroom/hero". */
  command?: string;
  /** center (default): centred from 640px up. start: left-aligned, when the sections after it are. */
  align?: 'center' | 'start';
  /** default: a page's opening. compact: the head of an inner page, a smaller h1 and less air. */
  size?: 'default' | 'compact';
};
hero.tsx
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Fragment } from 'react';
import { commandParts, hero, partClasses, type HeroContent } from './hero.classes';

export type HeroProps = HeroContent & { className?: string };

/** The opening section of a page: the h1, one sentence and at most two actions. */
export function Hero({ eyebrow, title, description, primary, secondary, command, align = 'center', size = 'default', className }: HeroProps) {
  return (
    <section className={cn(hero.root[size], className)}>
      <div className={hero.inner[align]}>
        {eyebrow && <Badge variant="outline">{eyebrow}</Badge>}
        <h1 className={hero.title[size]}>{title}</h1>
        {description && <p className={hero.description[size]}>{description}</p>}
        {(primary || secondary) && (
          <div className={cn(hero.actions[align], hero.actionsGap[size])}>
            {primary && <Button href={primary.href} size="lg">{primary.label}</Button>}
            {secondary && <Button href={secondary.href} size="lg" variant="outline">{secondary.label}</Button>}
          </div>
        )}
        {command && (
          <p className={hero.command}>
            <span aria-hidden="true">$</span>
            <code>
              {commandParts(command).map((word, w) => (
                <Fragment key={w}>
                  {w > 0 && ' '}
                  {word.map((part, k) => (
                    <Fragment key={k}>
                      {k > 0 && <wbr />}
                      <span className={partClasses}>{part}</span>
                    </Fragment>
                  ))}
                </Fragment>
              ))}
            </code>
          </p>
        )}
      </div>
    </section>
  );
}