Tessli

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.

  • Section
  • Hero
  • Community

Community — not yet reviewed by a maintainer

Preview

Theme
Width

1440 px wide. Open this preview on its own ↗

Install

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

Run it in a project set up with shadcn/ui. It adds product-hero.tsx to your components folder, plus the shadcn/ui button component if you don’t have it, and the item takes on your project’s theme.

When to use it

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.

Common AI default vs. this

The common 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.

What this item does instead

  • 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.

Why it is built this way

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.

Rules

How it breaks

Common mistakes when adapting this item.

  • 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, responsive behaviour and accessibility

States
default
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.

Code

web/registry/items/product-hero/product-hero.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>
  );
}

Credits and licence

Contributors
None credited yet.
Provenance
Original work
Licence
MIT
Review
Community — not yet reviewed by a maintainer

For agents

The same taste notes and code as structured data.