---
title: Hero
description: The head of a documentation page - trail, element name, version, measured facts, actions and a picture of itself.
source: https://adamjurek.com/components/hero
---

## Home


The top of every component and package page is this one component. Before it,
four pages assembled their own head out of a breadcrumb, an `h1`, a paragraph
and a row of chips, and all four disagreed about the order.

<!-- ::start:showcase demo="hero" height="460" -->
<!-- ::end:showcase -->

## It folds by room, not by window

The two-column layout is a container query, and `Hero` puts `.cq` on itself so
there is always something to measure.

The same hero renders in the full width of a component page and inside a 22rem
phone frame in the archive. A viewport query would give the phone frame two
columns of four words each, because the window it is being viewed in is wide.

```css
@container (min-width: 52rem) {
	.hero-split[data-shot] {
		grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
	}
}
```

`data-shot` is what gates it. A hero with no picture has nothing to put in the
second column, and an empty grid track is a gutter that reads as a mistake.

## The name is written as a tag

An element in this library is a tag before it is a page, so the heading says
`<avatar>` rather than "Avatar". The brackets are dimmed, which is the whole
trick: the name stays the thing your eye lands on while the punctuation does
the work of saying what kind of thing it is.

Pages that are not elements pass `title` instead and get an ordinary heading.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/hero.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/hero.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | docs · Page furniture |
| Files | `hero.tsx` |
| Dependencies | None |
| Also installs | `icon` |
| Tags | container-query, responsive, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


## Use it

```tsx
import { Hero } from "@sushindustries/ui";

export function Page() {
	return (
		<Hero
			title="Field"
			summary="A label, a control, a hint that only shows up when it matters."
			actions={<a href="#install">Install</a>}
		/>
	);
}
```

## What you should see

A heading with the title, the summary paragraph under it, and the action
below that - stacked in one column until there's a `shot` or `media` prop to
put beside it, at which point the layout splits into two.

## If nothing happens

`title` is the only required prop; a `Hero` with nothing else still renders a
heading. If the second column is missing when you expected one, check
`shot` and `media` aren't both set - `shot` wins and `media` is ignored
whenever both are given.


## Guides


## Slots, not data

`Hero` takes React nodes for the parts that know about routes - the breadcrumb
trail and the buttons - and plain values for the parts that do not. That split
is deliberate: a component in this library must never know that this site has
a `/components` route, and a component that took a `slug` and built its own
links would know exactly that.

```tsx
<Hero
	trail={<Breadcrumb items={trail} />}
	name="avatar"
	version="0.1.0"
	title="Avatar"
	summary="A face, its initials, or the tone of the group it belongs to."
	actions={
		<Button href="/components/avatar" variant="ghost">
			Docs
		</Button>
	}
/>
```

What it does own is the arrangement. Which things sit beside which, what wraps
first, and what the layout does when there is no picture.

## The facts are a definition list

Three or four labelled facts about the document - when it was last touched,
how long it takes to read, whether an agent can fetch it - are a definition
list, because that is what a definition list is.

The labels are visually hidden rather than absent. A calendar glyph beside a
date is unambiguous to anyone who can see it and silent to anyone who cannot,
so the `dt` carries the word and the `dd` carries the glyph.

> [!NOTE] `display: contents` on the group
> Each `dt`/`dd` pair needs a wrapper for the list to be valid, and a wrapper
> would become the flex item - putting the gap between wrappers instead of
> between facts. `display: contents` lifts the pair back into the row and keeps
> the nesting intact.

## The shot is eager

The picture is the largest thing above the fold, which makes it the Largest
Contentful Paint element on every page that has one. It is `fetchPriority`
high and never lazy, and the frame carries an `aspect-ratio` so the box exists
before the bytes do.

The sources are a `srcset` with `w` descriptors, taken at the three widths in
`devices.md`. A phone that downloads the laptop capture has paid for four
times the pixels it can show.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `variant?` | `"doc" \| "landing"` | `"doc"` | Which job this hero is doing. `landing` is the top of a home page: full height, the mark beside the sentence, one action and one alternative. `doc` is the head of a documentation page. They share the split, the actions row and the wrap order, and differ in height and type scale - which is why they are one component with an attribute rather than two components. |
| `trail?` | `ReactNode` | - | Above the heading. A breadcrumb, usually. |
| `name?` | `string` | - | The element's own id, rendered as `<name>`. A component in this library is a tag before it is a page, and writing it the way it is written in markup is the shortest true description of what the reader has arrived at. When it is absent the heading falls back to `title`, which is what a page that is not an element wants. |
| `title` | `string` | - |  |
| `version?` | `string` | - | Shown as a chip beside the heading. The element's version, not the package's. |
| `summary?` | `ReactNode` | - | One paragraph under the heading. Absent puts the facts straight beneath it. |
| `facts?` | `readonly HeroFact[]` | - | Keyed by `label`, so two facts cannot share one. Empty renders no list at all. |
| `actions?` | `ReactNode` | - | The one or two things to do here. Composed by the caller. |
| `shot?` | `HeroShot` | - | A picture of the thing, taken at each device width. |
| `media?` | `ReactNode` | - | Anything else for the second column - a 3D mark, a live frame, a chart. Ignored when `shot` is given, because a hero has one second column and two things fighting for it is a bug rather than a layout. |
| `children?` | `ReactNode` | - | Below everything, full width. The section tabs, usually. |

<!-- /generated:api -->

## Notes

`shot` and `media` share one second column, and `shot` wins when both are
given - a hero has one picture, not two fighting for the same space. Pass
whichever fits: `shot` for a captured image with a real `srcset`, `media` for
anything else that belongs there (a live frame, a 3D mark, a chart).

`name` and `title` are not the same field said twice. `name` renders as
`<name>` - the element's own identifier, for a component's own hero - and
falls back to `title` when absent. A page that is not an element (the home
page, a post) sets `title` alone.

`variant` changes height and type scale, not layout - `doc` and `landing`
share the split, the actions row and the wrap order. Reach for `landing` only
at the top of a page that stands in for the whole site; everywhere else,
`doc` is correct even when nothing else about the page is document-shaped.


## Examples


<!-- ::start:showcase demo="hero" height="420" -->
<!-- ::end:showcase -->

Press Compare. A hero is the largest thing above the fold on most pages it
sits on, so it is worth checking it does not overflow at 320.

## In a page

```tsx
import { Hero, Breadcrumb } from "@sushindustries/ui";

export function ComponentPage() {
	return (
		<Hero
			variant="doc"
			trail={<Breadcrumb items={[{ label: "Components", href: "/components" }]} />}
			name="hero"
			title="Hero"
			version="0.1.0"
			summary="The top of a documentation page, as one component."
		/>
	);
}
```

## What this example is not

The demo shows `Hero` on its own, at the width the showcase frame gives it.
On a real page it sits inside `apps/web`'s document layout, which is what
actually constrains its max width - `Hero` itself has no opinion on that.
