Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
eyebrowstringSmall uppercase line above the title.
titleReactNodeThe headline. A string or your own element.
descriptionReactNodeOne or two sentences under the title.
actionsReactNodeButtons 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.
childrenReactNodeOptional 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