Hero split
A hero with the copy on one side and the product on the other.
npx shadcn@latest add https://ui.goodtabs.co/r/hero-split.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/hero-split
Analytics
Know what your users did this morning
Dashboards that read from your own database. No pixels, no sampling.
Eyebrow, title, two actions and a placeholder for the product.
Product on the left
Design tokens
One source for color, type and space
Edit a token once and every component follows, in light and dark.
side="left" flips the order from the container's 2xl width.
Usage
import { HeroSplit } from "@/components/hero-split"
import { Button } from "@/components/ui/button"
<HeroSplit
eyebrow="Analytics"
title="Know what your users did this morning"
description="Dashboards that read from your own database."
actions={<Button>Start free</Button>}
>
<img src="/product.png" alt="Dashboard" className="rounded-lg border" />
</HeroSplit>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 plain link. | |
| side | "right" | "left" | "right" | Which side the product sits on from the container's 2xl width up. |
| children | ReactNode | The product: a browser frame, a screenshot, a demo. |
Source
hero-split/hero-split.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
type HeroSplitProps = 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 plain link. */
actions?: React.ReactNode;
/** Which side the product sits on from the container's 2xl width up. */
side?: "right" | "left";
/** The product: a browser frame, a screenshot, a demo. */
children?: React.ReactNode;
};
/**
* A hero with the copy on one side and the product on the other.
* Splits by container width, not viewport, so it also works inside a card or a column.
* Server component. Everything is a slot.
*/
export function HeroSplit({
eyebrow,
title,
description,
actions,
side = "right",
children,
className,
...props
}: HeroSplitProps) {
return (
<section className={cn("@container w-full py-8 @2xl:py-16 @5xl:py-24", className)} {...props}>
<div className="mx-auto grid w-full max-w-6xl items-center gap-8 px-4 @2xl:grid-cols-2 @2xl:gap-12 @2xl:px-6">
<div className={cn("flex flex-col items-start gap-4 @2xl:gap-5", side === "left" && "@2xl:order-2")}>
{eyebrow && <p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>}
<h1 className="m-0 text-2xl leading-none font-medium tracking-tight text-balance @2xl:text-4xl @5xl:text-5xl">
{title}
</h1>
{description && (
<p className="m-0 max-w-md text-sm text-muted-foreground @2xl:text-base @5xl:text-lg">{description}</p>
)}
{actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
</div>
{children && <div className="min-w-0">{children}</div>}
</div>
</section>
);
}
For AI agents
Hand your agent this page, the markdown at /components/hero-split/prompt.md, or the Copy AI prompt button above.
When to use
- The top of a product page where the screenshot is the proof
- When the headline is short and the product needs real width
- Inside a column or card, since it splits by container width
When not to use
- A centered landing hero with no product, which wants the hero
- When the visual is a wide banner, which wants a full-width frame below the copy
- More than once on a page
Agent notes
- Splits on container width (@2xl), not viewport, so it also works in a narrow column or card
- Server component; pass buttons from @/components/ui/button into actions
- Stacks with the copy first below 42rem, whatever side is set
- The section carries its own vertical padding and a max-w-6xl container, so place it directly in main