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).
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@
Plans for every stage
A smaller h1 and less air, for the head of an inner page.
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@
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.
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';
};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>
);
}---
import Badge from '@/components/ui/badge.astro';
import Button from '@/components/ui/button.astro';
import { cn } from '@/lib/utils';
import { commandParts, hero, partClasses, type HeroContent } from './hero.classes';
type Props = HeroContent & { class?: string };
const { eyebrow, title, description, primary, secondary, command, align = 'center', size = 'default', class: className } = Astro.props;
---
<section class={cn(hero.root[size], className)}>
<div class={hero.inner[align]}>
{eyebrow && <Badge variant="outline">{eyebrow}</Badge>}
<h1 class={hero.title[size]}>{title}</h1>
{description && <p class={hero.description[size]}>{description}</p>}
{
(primary || secondary) && (
<div class={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 class={hero.command}>
<span aria-hidden="true">$</span>
<code>{commandParts(command).map((word, w) => <>{w > 0 && ' '}{word.map((part, k) => <>{k > 0 && <wbr />}<span class={partClasses}>{part}</span></>)}</>)}</code>
</p>
)
}
</div>
</section>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';
};<section class="bg-background px-6 py-20 sm:py-28">
<div class="mx-auto flex max-w-3xl flex-col items-start text-left sm:items-center sm:text-center">
<span class="inline-flex w-fit shrink-0 items-center gap-1 rounded-sm border px-1.5 py-0.5 text-xs font-medium whitespace-nowrap [&_svg]:size-3 border-border text-foreground">
Open source
</span>
<h1 class="mt-5 text-4xl leading-[1.05] font-semibold tracking-[-0.035em] text-balance sm:text-6xl">Ship the page, not the boilerplate</h1>
<p class="mt-5 max-w-xl text-base text-pretty text-muted-foreground sm:text-lg">
A starting point for your product site: clear type, two actions and an install command your readers can copy.
</p>
<div class="flex flex-wrap gap-3 sm:justify-center mt-8">
<a href="#" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-11 px-5 text-[15px]">
Get started
</a>
<a href="#" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-11 px-5 text-[15px]">
View on GitHub
</a>
</div>
<p class="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]">
<span aria-hidden="true">$</span>
<code><span class="whitespace-nowrap">npx</span> <span class="whitespace-nowrap">create-my-app@</span><wbr><span class="whitespace-nowrap">latest</span></code>
</p>
</div>
</section>
<section class="bg-background px-6 py-12 sm:py-16 border-t">
<div class="mx-auto flex max-w-3xl flex-col items-start text-left sm:items-center sm:text-center">
<span class="inline-flex w-fit shrink-0 items-center gap-1 rounded-sm border px-1.5 py-0.5 text-xs font-medium whitespace-nowrap [&_svg]:size-3 border-border text-foreground">
Pricing
</span>
<h1 class="mt-4 text-3xl leading-[1.1] font-semibold tracking-[-0.03em] text-balance sm:text-5xl">Plans for every stage</h1>
<p class="mt-4 max-w-xl text-base text-pretty text-muted-foreground">A smaller h1 and less air, for the head of an inner page.</p>
</div>
</section>