# Entity Header

> A React entity header for record pages (a client, a deal, an invoice): name, status, key facts, actions, favorite, follow and a pager.

- Type: React component (Layout)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.2.0
- Install: `npx wingo-ui@latest add entity-header`
- Docs: https://wingo-ui.com/components/entity-header

## Interaction

Read the record at a glance: type above the name, status badges, and a strip of key facts (Sold, Responsabil, CUI...) that scrolls sideways on a phone with soft edge fades; when it overflows, Tab reaches it and the arrow keys scroll it. Hover or focus a fact to reveal its copy button (always visible on touch). Double click the name (or press F2 on it, or tap the pencil on touch) to rename it: Enter saves, Escape cancels, an empty name is refused and a failed save rolls back with the error under it while the star and badges stay on the name line. Tap the star to favorite the record (on a phone it stays next to the name whenever it fits); Follow turns into Following with a bell that rings once when the change lands. The primary action stays in view and the rest fold into ⋯ as the header narrows, a bottom action sheet on phones, with the follow toggle inside. The up / down arrows next to "3 of 12" walk to the previous and next record (J / K in the demo, shown in their tooltips; keys never fire while typing), and at the first or last record the arrow is disabled; a phone shows only the arrows. Scroll past the header and a glass bar with the name and the primary action slides down under the top bar. In grid and tiles layouts a phone shows the first maxFacts facts and a Show all (n) button that opens the rest.

## Installation

```bash
npx wingo-ui@latest add entity-header
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/entity-header` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/entity-header.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Breadcrumb](https://wingo-ui.com/components/breadcrumb) ([Markdown](https://wingo-ui.com/components/breadcrumb.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Favorite button](https://wingo-ui.com/components/favorite-button) ([Markdown](https://wingo-ui.com/components/favorite-button.md))
- [Inline Edit](https://wingo-ui.com/components/inline-edit) ([Markdown](https://wingo-ui.com/components/inline-edit.md))
- [Page Header](https://wingo-ui.com/components/page-header) ([Markdown](https://wingo-ui.com/components/page-header.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Mail, Plus, Trash2 } from "lucide-react"
import { EntityHeader, type EntityFact } from "@/components/ui/entity-header"
import { NavTabs } from "@/components/ui/nav-tabs"

const facts: EntityFact[] = [
  { label: "Balance", value: "$12,480" },
  { label: "Owner", value: "Emma Carter" },
  { label: "Tax ID", value: "12-3456789", copyValue: "12-3456789" },
  { label: "City", value: "Chicago" },
  { label: "Client since", value: "Mar 12, 2024" },
]

export function ClientHeader({ index, onMove, onRename }: { index: number; onMove: (index: number) => void; onRename: (name: string) => Promise<void> }) {
  return (
    <EntityHeader
      breadcrumb={[{ label: "Clients", href: "/clients" }, { label: "Tech Solutions Inc." }]}
      eyebrow="Client · Company"
      title="Tech Solutions Inc."
      onTitleChange={onRename}
      avatar={{ name: "Tech Solutions Inc.", radius: "md" }}
      status={[{ label: "Active", tone: "success" }, { label: "VIP", tone: "brand" }]}
      facts={facts}
      actions={[
        { label: "Send email", icon: <Mail /> },
        { label: "Delete", icon: <Trash2 />, tone: "danger" },
        { label: "New invoice", icon: <Plus />, primary: true, href: "/invoices/new" },
      ]}
      showFollow
      pager={{ index, total: 12, onPrevious: () => onMove(index - 1), onNext: () => onMove(index + 1) }}
      previousShortcut="k"
      nextShortcut="j"
      locale="en-US"
      labels={{ follow: "Watch", following: "Watching" }}
      tabs={<NavTabs items={[{ label: "Overview", href: "/clients/tech-solutions" }, { label: "Invoices", href: "/clients/tech-solutions/invoices", badge: 12 }]} />}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` | `""` | The record name, rendered as the page's one <h1> (headingAs changes it; also shown in the condensed bar). The playground shows Tech Solutions Inc. while it is empty. |
| `headingAs?` | `"h1" \| "h2" \| "h3" \| "div"` | `"h1"` | The title's element. Keep h1 on the record's page; use h2, h3 or div when the header sits inside a page that already has its h1. |
| `onTitleChange?` | `(title: string) => (void \| Promise<unknown>)` |  | Makes the title editable through InlineEdit: a double click or F2 (a pencil button on touch). Called with the trimmed name, which shows optimistically; a rejection restores it and shows the error's message (else labels.renameError) under it. An empty name is refused with labels.titleRequired. |
| `eyebrow?` | `ReactNode` | `""` | The record type in small caps on its own row above the name, like "Client · Company". It stays outside the h1 (read right after the name), so the heading is the name alone. |
| `description?` | `ReactNode` | `""` | One line under the title (page-header's description). |
| `avatar?` | `PageHeaderAvatar` |  | A photo or initials before the title: { src?, name, radius? }. Companies use radius "md" (a rounded square), people "full". |
| `icon?` | `ReactNode` |  | A tinted icon tile before the title when there is no avatar (a deal, an invoice). |
| `status?` | `EntityStatus \| EntityStatus[]` |  | One or several status badges after the title, each { label, tone?, icon? }: a soft badge with a dot (or the icon) in its tone, so color is never the only signal. |
| `facts?` | `EntityFact[]` |  | Key facts under the title, each { id?, label, value, icon?, href?, copyValue?, hint? }. href turns the value into a link, copyValue adds a copy button (the value may be any node, copyValue is the text copied), hint is a quiet line under it. None: no strip. |
| `factsLayout?` | `"strip" \| "grid" \| "tiles"` | `"strip"` | strip: one row with hairline dividers that scrolls sideways (with edge fades) when it does not fit. grid: 2, 3 or up to 6 columns by the header's width. tiles: the grid in flat gray tiles. |
| `maxFacts?` | `number` | `0` | Grid and tiles on phones: show this many facts, then a Show all (n) button that reveals the rest. 0 shows all. |
| `actions?` | `PageHeaderAction[]` |  | page-header's actions: the primary (primary: true, else the last solid one) stays visible, the rest fold into ⋯ from the end as the header narrows; a bottom action sheet on phones. A returned promise keeps the button loading. |
| `favorite?` | `boolean` |  | Controlled star state. Setting it also shows the star. |
| `defaultFavorite?` | `boolean` | `false` | The star's starting state while uncontrolled. |
| `onFavoriteChange?` | `(favorite: boolean) => (void \| Promise<unknown>)` |  | A returned promise keeps the star's optimistic state while pending; a rejection rolls it back, shakes and announces labels.favoriteError. |
| `showFavorite?` | `boolean` | `true` | The star after the title (a FavoriteButton with a Star in the tone). |
| `following?` | `boolean` |  | Controlled follow state. Setting it also shows the toggle. |
| `defaultFollowing?` | `boolean` | `false` | The follow toggle's starting state while uncontrolled. |
| `onFollowingChange?` | `(following: boolean) => (void \| Promise<unknown>)` |  | Called with the next state. The toggle flips at once; a returned promise keeps the button loading, and a rejection flips it back and announces labels.followError. |
| `showFollow?` | `boolean` | `false` | A Follow / Following toggle (Bell / BellRing, aria-pressed) that leads the secondary actions. It folds into ⋯ like them (last, since actions fold from the end) and its bell rings once when turned on. |
| `pager?` | `EntityPager` |  | Previous / next record: { index (0-based), total, onPrevious?, onNext?, previousHref?, nextHref?, label? } renders "3 of 48" and two arrow buttons at the end of the breadcrumb row. Hidden when total is 0 or 1; the arrow at either end is disabled. On phones only the arrows show. |
| `previousShortcut?` | `string` | `""` | A key for the previous record in mod+k notation ("k" in the demo), shown in the arrow's tooltip and ignored while typing. Empty binds nothing. |
| `nextShortcut?` | `string` | `""` | A key for the next record ("j" in the demo). |
| `breadcrumb?` | `BreadcrumbEntry[] \| ReactNode` |  | page-header's trail above the title; the pager sits at the end of its row. |
| `backHref?` | `string` | `""` | A back button before the title (a ‹ link on phones, where it replaces the breadcrumb; the pager stays). |
| `onBack?` | `() => void` |  | Runs when the back button is pressed (renders it without backHref). |
| `tabs?` | `ReactNode` |  | NavTabs (or a Tabs list) on the header's bottom border. |
| `meta?` | `PageHeaderMeta[] \| ReactNode` |  | page-header's small wrapping meta row under the description; facts are the richer choice for records. |
| `children?` | `ReactNode` |  | Extra content after the facts, before the tabs (a progress bar, a notice). |
| `sticky?` | `"none" \| "condensed"` | `"condensed"` | condensed: once the header scrolls away a glass bar with the avatar, the name and the primary action pins under the top bar. |
| `maxActions?` | `number` | `0` | The most buttons in the wide row, the primary included (1: only the primary, the rest and the follow toggle in ⋯). 0: as many as fit. |
| `mobileActions?` | `"menu" \| "stack"` | `"menu"` | The compact layout (header under 512px): secondary actions and the follow toggle in the ⋯ action sheet, or stacked full width. |
| `border?` | `boolean` | `true` | The hairline under the header. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | page-header's sizes (18 / 24 / 32px titles, 18 / 22 / 26px on phones) with fact values at 13 / 14 / 15px. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The avatar initials or icon tile color, and the star's on color. neutral keeps the name-based avatar color and a black / white star. |
| `color?` | `string` | `""` | Any CSS color for the avatar / icon tile and the star; replaces the tone. |
| `loading?` | `boolean` | `false` | page-header's skeletons plus skeleton facts in the same layout (4 while facts is still an empty array). |
| `locale?` | `string` | `""` | Formats the pager count and the Show all count ("1,234 of 5,000" in en-US). Empty follows the UIProvider, then the browser. |
| `labels?` | `Partial<EntityHeaderLabels>` |  | Every string: favorite, addFavorite, removeFavorite, favoriteError, follow, following, followError, previous, next, pager(position, total), showAllFacts(count, formatted), showFewerFacts, copy, copied, copyError, rename, renameError, titleRequired, facts, back, more, cancel. The error strings are announced when a change rolls back. English defaults in DEFAULT_ENTITY_HEADER_LABELS. |
| `scrollTarget?` | `HTMLElement \| Window \| RefObject<HTMLElement \| null> \| null` |  | What scrolls, for the condensed bar. Default: the nearest ScrollContainerProvider, else the nearest scrolling parent, else the window. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` |  | The router link for the trail, the actions, fact links and pager hrefs. Default: the LinkProvider's. |
| `classNames?` | `Partial<Record<"facts" \| "fact" \| "pager" \| "title" \| "eyebrow" \| "badges", string>>` |  | Extra classes for the facts block, each fact, the pager, the title, the eyebrow and the status badges group. |
| `pageHeaderClassNames?` | `Partial<Record<PageHeaderSlot, string>>` |  | Passed to page-header's classNames (row, heading, actions, tabs, condensed...). Its badges span is display: contents here (the eyebrow, star and badges lay out in the heading row), so style the badges through classNames.badges. |
| `className?` | `string` |  | Classes for the header box, which also takes id and style. The data-slot="entity-header" wrapper is display: contents, so the condensed bar stays a child of the scrolling page and can stick; the other rest props (aria-*, data-*, handlers) go on it. |

## Parts

### EntityFacts (component)

The facts block alone, for a card or a side panel: facts, layout, maxFacts, size, loading, skeletonCount, locale, labels, linkComponent, classNames (list, fact, label, value, hint, more). Props: EntityFactsProps.

### DEFAULT_ENTITY_HEADER_LABELS (constant)

The English strings, to spread under a translation. Type: EntityHeaderLabels.

## Examples

- **A contact**: A round avatar, eyebrow Contact, grid facts with mailto / tel links and copy buttons.
- **A deal**: An icon tile in the warning tone, tiles facts (4 on phones, then Show all), and a primary that marks it won.
- **An invoice**: A back link instead of a breadcrumb, a warning status and a node as a fact value (the e-invoice badge).
- **Facts layouts**: The same six facts as strip, grid and tiles (EntityFacts on its own).
- **Loading**: page-header's skeletons plus skeleton facts in the strip's shape.
- **Phone**: The compact layout: the primary stays, the rest and the follow toggle open from ⋯, the facts scroll as a rail.

## See also

- [Page Header](https://wingo-ui.com/components/page-header) ([Markdown](https://wingo-ui.com/components/page-header.md))
- [Contact card](https://wingo-ui.com/components/contact-card) ([Markdown](https://wingo-ui.com/components/contact-card.md))
- [Nav Tabs](https://wingo-ui.com/components/nav-tabs) ([Markdown](https://wingo-ui.com/components/nav-tabs.md))
- [Favorite button](https://wingo-ui.com/components/favorite-button) ([Markdown](https://wingo-ui.com/components/favorite-button.md))
- [Inline Edit](https://wingo-ui.com/components/inline-edit) ([Markdown](https://wingo-ui.com/components/inline-edit.md))

---

Canonical: https://wingo-ui.com/components/entity-header
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
