Skip to content
    Style: Wireframe

    Blocks · Marketing

    Hero

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

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

    Used in UI Pro: Hero

    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/ui/r/hero.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/hero.json

    Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI 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>
      );
    }