Hero
A marketing hero with eyebrow, title, actions, a stats strip and a visual slot.
npx shadcn@latest add https://ui.goodtabs.co/r/hero.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/hero
goodtabs UI
Production UI, picked by a person
Free shadcn components in one calm look, installed by your agent with one command.
- components
- 200
- categories
- 10
- licence
- MIT
Eyebrow, title, two actions and three numbers, centered.
Left aligned with a visual
Analytics
Know what your users did this morning
Realtime dashboards that read from your own database. No pixels, no sampling, no waiting until tomorrow.
Left aligned, with a placeholder where a screenshot frame goes.
Minimal
The tabs worth keeping open
Just a title and one action.
Usage
import { Hero } from "@/components/hero"
import { Button } from "@/components/ui/button"
<Hero
eyebrow="goodtabs UI"
title="Production UI, picked by a person"
description="Free shadcn components in one calm look."
actions={<><Button>Browse components</Button><Button variant="outline">Docs</Button></>}
stats={[{ value: "200", label: "components" }, { value: "MIT", label: "licence" }]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| eyebrow | string | Small uppercase line above the title. | |
| title | ReactNode | The headline. A string or your own element. | |
| description | ReactNode | One or two sentences under the title. | |
| actions | ReactNode | Buttons and links, usually one primary and one outline. | |
| align | "center" | "left" | "center" | Centered for a landing page, left for a product page. |
| size | "sm" | "default" | "lg" | "default" | Title scale and vertical rhythm. sm fits a card or narrow column. |
| stats | { value: string; label: string }[] | Three or four numbers under the actions. | |
| children | ReactNode | Optional visual below: a screenshot frame, a demo, an image. |
Source
hero/hero.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
type Stat = { value: string; label: string };
type HeroProps = Omit<React.ComponentProps<"section">, "title"> & {
/** Small uppercase line above the title. */
eyebrow?: string;
/** The headline. Pass a string or your own element. */
title: React.ReactNode;
/** One or two sentences under the title. */
description?: React.ReactNode;
/** Buttons and links, usually one primary and one outline. */
actions?: React.ReactNode;
/** Centered for a landing page, left for a product page. */
align?: "center" | "left";
/** Title scale. "sm" fits a card or a narrow column. */
size?: "sm" | "default" | "lg";
/** Three or four numbers under the actions. */
stats?: Stat[];
/** Optional visual below: a screenshot frame, a demo, an image. */
children?: React.ReactNode;
};
const titleSize = {
sm: "text-2xl sm:text-3xl",
default: "text-4xl sm:text-5xl",
lg: "text-4xl sm:text-6xl lg:text-7xl",
};
const padding = {
sm: "py-8 sm:py-10",
default: "py-16 lg:py-24",
lg: "py-16 lg:py-24",
};
/**
* A marketing hero: eyebrow, title, description, actions, an optional stats strip and an optional visual.
* Server component. Everything is a slot, so the content stays yours.
*/
export function Hero({
eyebrow,
title,
description,
actions,
align = "center",
size = "default",
stats,
children,
className,
...props
}: HeroProps) {
const centered = align === "center";
return (
<section className={cn(padding[size], className)} {...props}>
<div className="mx-auto w-full max-w-6xl px-4 md:px-6">
<div className={cn("flex flex-col gap-5", centered ? "items-center text-center" : "items-start text-left")}>
{eyebrow && <p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>}
<h1 className={cn("m-0 max-w-4xl leading-none font-medium tracking-tight text-balance", titleSize[size])}>
{title}
</h1>
{description && (
<p
className={cn(
"m-0 max-w-2xl text-muted-foreground",
size === "sm" ? "text-sm sm:text-base" : "text-base sm:text-lg",
)}
>
{description}
</p>
)}
{actions && <div className={cn("flex flex-wrap gap-2", centered && "justify-center")}>{actions}</div>}
{stats && stats.length > 0 && (
<dl
className={cn(
"m-0 mt-3 grid gap-6",
centered ? "justify-center text-center" : "text-left",
stats.length >= 4 ? "grid-cols-2 sm:grid-cols-4" : "grid-cols-3",
)}
>
{stats.map((s) => (
<div key={s.label} className="flex flex-col-reverse gap-0.5">
<dt className="text-xs text-muted-foreground">{s.label}</dt>
<dd className="m-0 text-2xl font-medium tracking-tight">{s.value}</dd>
</div>
))}
</dl>
)}
</div>
{children && <div className={cn("mt-10 lg:mt-14", centered && "mx-auto")}>{children}</div>}
</div>
</section>
);
}
For AI agents
Hand your agent this page, the markdown at /components/hero/prompt.md, or the Copy AI prompt button above.
When to use
- The top of a landing or product page, one per page
- When the message is a title, a line of support and one or two actions
- With a stats strip when three or four numbers make the case
When not to use
- Inside app screens or dashboards, where a page title is enough
- For a list of features or pricing, which have their own sections
- More than once on a page
Agent notes
- Server component with no state; pass buttons from @/components/ui/button into actions
- Default size uses text-4xl sm:text-5xl; lg adds lg:text-6xl; sm is for cards and previews
- children render below the text with mt-10, meant for a browser-frame or screenshot
- The section carries py-16 lg:py-24 and a max-w-6xl container, so place it directly in main