Skip to content
goodtabs UI
BrowseBrowse components

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

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 plain link.
side"right" | "left""right"Which side the product sits on from the container's 2xl width up.
childrenReactNodeThe 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