# Tessli library item — Product hero with real UI

A landing-page hero that says what the product does, offers one primary action and shows the product itself, composed from interface components.

Kind: section
Category: hero
Status: community (not yet reviewed by a maintainer)
Tessli page: /library/product-hero
Preview: /preview/product-hero

## Install

```sh
npx shadcn@latest add online-scope-lab/tessli/product-hero
```

## Use when

- The top of a landing page for a product people use on screen, where seeing the interface answers "what is it?" faster than copy.
- You can compose a representative view of the product from your own components and tokens, with content that looks like real use.

## Avoid when

- The product has no interface worth showing (an API, a service); show a short code sample or the outcome instead.
- The view you would show is not built yet; describe what exists and leave the frame out rather than mocking up features.
- A page deeper in the site (docs, pricing); use a plain heading and description there, not a hero.

## Rules

- H-01 — Give each section, card or dialog at most one primary-styled action. (/rules/H-01)
- W-01 — Write headings that make the product's point, not labels like "Features" or "FAQ". (/rules/W-01)
- W-02 — Label actions with what happens next, not "Get started", "Submit" or "Learn more". (/rules/W-02)
- W-03 — Show no ratings, counts, logos, quotes or metrics you can't source. (/rules/W-03)
- C-03 — Spend the primary colour on the primary action and real status, not decoration. (/rules/C-03)
- C-04 — Show importance with position, size, weight and borders, not gradients, glows or glass. (/rules/C-04)
- S-02 — Align a section's heading, text and actions on one left edge; centre only short blocks. (/rules/S-02)
- R-01 — On phones, restack content by priority and adapt components; don't shrink the desktop. (/rules/R-01)

## Common AI default

A centred headline such as "Supercharge your workflow" over a purple-to-blue gradient blob, a vague subline, two equal buttons ("Get started" and "Learn more"), a row of grey customer logos and "Trusted by 10,000+ teams", and a tilted, glowing screenshot or an abstract 3D shape where the product should be.

## Why this is better

- The headline says what the product does for whom, so a visitor knows within one line whether to keep reading.
- One primary button names what happens next; the secondary route is a quiet underlined link, so the two never compete.
- The product is shown as it is used, with a notebook open and believable notes, instead of a gradient or a decorative shape standing in for it.
- The frame is built from the same semantic tokens as the page, so it follows the theme (light or dark) and stays sharp at every width, unlike a screenshot.
- The frame recomposes as it narrows (the list alone on phones, then the open note, then the notebooks column) instead of shrinking a desktop picture until its text is unreadable.
- The terms people check before clicking ("Free for one person. No card needed.") sit under the action, and there are no logos, counts or quotes the page cannot source.

## Common failures when adapting it

- Mocking up features the product does not have, or content that no user would write, so the frame promises more than the product does.
- Adding a second primary button, or styling the secondary link as a full outline button so the choice becomes equal.
- Swapping the frame for a screenshot that turns into unreadable text on phones and ignores the theme.
- Putting real, focusable controls inside the frame; it is a picture, hidden from assistive technology.
- Filling the space under the actions with invented logos or "Trusted by" counts.
- Centring a long headline and description, which makes the text ragged and hard to scan.

## States

- default

## Responsive behaviour

- Desktop: Headline, description and actions on one left edge, capped at a readable width, with the product frame full width below; from 56rem of frame width it shows the notebooks column, the notes list and the open note.
- Mobile: The text block comes first and the actions stack full width with the primary on top; the frame below drops to the notes list alone, at full text size, and adds the open note from 36rem.

## Accessibility

- The section is labelled by its heading; headingLevel is 1 on a landing page (the hero holds the page title) and 2 where the page already has an h1, as in this preview.
- Both actions are links to their destinations, at least 44px tall with a visible focus ring; the secondary is underlined, so it reads as a link without relying on colour.
- The product frame is a figure whose visible caption is its text alternative; the composed interface inside it is aria-hidden and inert, so it adds no tab stops and no duplicate reading.
- Selection in the frame is shown with a left bar and weight as well as a background tint, so it survives greyscale.
- Nothing animates.

## Notes

The default hero spends its space on atmosphere: a gradient, a slogan that fits any product and two buttons that ask for the same decision. Visitors leave it knowing nothing about the product. This item spends the space on the product: a headline that says what it does, one action, and the interface itself.

The frame is not a screenshot. `ProductWindow` takes a static composition built from your own components and tokens, so it follows the page theme, stays crisp at any width, and can **recompose** as it narrows: its content area is a size container, so the demo shows the notes list alone on a phone and adds the open note and the notebooks column as room allows.

Keep the frame honest. Show a view that exists, with content a real user would write, and write a caption that says what it shows. Leave out what you cannot source: no logos, user counts or quotes. One primary action, one quiet alternative, and the terms people check before they click.

## Credits

- Contributors: none credited yet
- Provenance: original
- Licence: MIT

## Source

`web/registry/items/product-hero/product-hero.tsx`

```tsx
import * as React from "react";

import { cn } from "@/registry/shadcn/lib/utils";
import { buttonVariants } from "@/registry/shadcn/ui/button";

export type ProductHeroAction = {
  /** What happens next: "Start a free notebook", not "Get started". */
  label: string;
  href: string;
};

type HeadingLevel = 1 | 2;

export type ProductHeroProps = Omit<
  React.ComponentProps<"section">,
  "title"
> & {
  /**
   * What the product does, in plain words. If another product's name could
   * replace yours and the headline still works, rewrite it.
   */
  title: React.ReactNode;
  /** One or two sentences: who it is for and the two or three things it does. */
  description?: React.ReactNode;
  /** The only primary-styled action in the hero. */
  primaryAction: ProductHeroAction;
  /** An optional quieter route (a tour, the docs). Never a second primary. */
  secondaryAction?: ProductHeroAction;
  /** Terms people check before they click (price, trial, card). Only if true. */
  note?: React.ReactNode;
  /** The product itself: a ProductWindow composed from your real UI. */
  product: React.ReactNode;
  /** 1 on a landing page, where the hero holds the page title; 2 otherwise. */
  headingLevel?: HeadingLevel;
};

export function ProductHero({
  title,
  description,
  primaryAction,
  secondaryAction,
  note,
  product,
  headingLevel = 1,
  className,
  ...props
}: ProductHeroProps) {
  const headingId = React.useId();
  const Heading = `h${headingLevel}` as const;

  return (
    <section
      aria-labelledby={headingId}
      data-slot="product-hero"
      className={cn("bg-background text-foreground", className)}
      {...props}
    >
      <div className="mx-auto w-full max-w-6xl px-4 py-12 sm:px-6 sm:py-16 lg:px-8 lg:py-20">
        <div className="max-w-3xl">
          <Heading
            id={headingId}
            className="text-4xl leading-[1.1] font-semibold tracking-tight text-balance sm:text-5xl"
          >
            {title}
          </Heading>
          {description ? (
            <p className="mt-5 max-w-prose text-lg leading-relaxed text-pretty text-muted-foreground">
              {description}
            </p>
          ) : null}

          <div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center">
            <a
              href={primaryAction.href}
              className={cn(
                buttonVariants({ size: "lg" }),
                "h-11 w-full px-6 sm:w-auto",
              )}
            >
              {primaryAction.label}
            </a>
            {secondaryAction ? (
              <a
                href={secondaryAction.href}
                className={cn(
                  buttonVariants({ variant: "ghost", size: "lg" }),
                  "h-11 w-full px-4 underline decoration-muted-foreground underline-offset-4 hover:decoration-current sm:w-auto",
                )}
              >
                {secondaryAction.label}
              </a>
            ) : null}
          </div>
          {note ? (
            <p className="mt-4 text-sm text-muted-foreground">{note}</p>
          ) : null}
        </div>

        <div className="mt-12 sm:mt-16">{product}</div>
      </div>
    </section>
  );
}

export type ProductWindowProps = Omit<
  React.ComponentProps<"figure">,
  "children"
> & {
  /** The title bar text: the product, workspace or view name. */
  label: React.ReactNode;
  /**
   * Visible caption and the text alternative for the whole frame. Say what
   * the frame shows; if the product or its content is fictional, say so.
   */
  caption: React.ReactNode;
  /**
   * A static composition of your product's UI, built from your own
   * components and tokens rather than a screenshot. It is a picture: it is
   * hidden from assistive technology and inert, so put no real controls in
   * it. The content area is a size container, so children can recompose
   * with @sm: … @3xl: variants as the frame narrows.
   */
  children: React.ReactNode;
};

export function ProductWindow({
  label,
  caption,
  children,
  className,
  ...props
}: ProductWindowProps) {
  return (
    <figure
      data-slot="product-window"
      className={cn("m-0 min-w-0", className)}
      {...props}
    >
      <div
        aria-hidden="true"
        inert
        className="overflow-hidden rounded-lg border border-border bg-card text-card-foreground select-none"
      >
        <div className="flex h-10 items-center border-b border-border bg-muted px-4">
          <span className="min-w-0 truncate text-xs text-muted-foreground">
            {label}
          </span>
        </div>
        <div className="@container">{children}</div>
      </div>
      <figcaption className="mt-3 max-w-prose text-sm text-pretty text-muted-foreground">
        {caption}
      </figcaption>
    </figure>
  );
}
```

## Interpretation boundaries

- Community items are merged and pass automated checks (schema, build, accessibility, overflow and console errors in three themes at five widths); only items with status reviewed have been checked by a maintainer.
- Each rule id links to its page at /rules/<id>; every rule is a draft until a maintainer signs it.
- Tessli does not claim that using an item improves AI-generated UI; that claim waits for a published evaluation.
