Entity Header React component for Tailwind CSS v4
v1.2.0Updated in 1.11.0
A React entity header for record pages (a client, a deal, an invoice): name, status, key facts, actions, favorite, follow and a pager.
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.
Entity Header is a Pro React component in the React layout components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add entity-header, together with Badge, Breadcrumb, Button, Copy Button, Favorite button and 14 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add entity-headerEntity Header is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import EntityHeader from @/components/ui/entity-header.
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
title*default""stringThe 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.
headingAsdefault"h1""h1" | "h2" | "h3" | "div"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.
eyebrowdefault""ReactNodeThe 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.
descriptiondefault""ReactNodeOne line under the title (page-header's description).
avatarPageHeaderAvatarA photo or initials before the title: { src?, name, radius? }. Companies use radius "md" (a rounded square), people "full".
iconReactNodeA tinted icon tile before the title when there is no avatar (a deal, an invoice).
statusEntityStatus | 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.
factsEntityFact[]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.
factsLayoutdefault"strip""strip" | "grid" | "tiles"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.
maxFactsdefault0numberGrid and tiles on phones: show this many facts, then a Show all (n) button that reveals the rest. 0 shows all.
actionsPageHeaderAction[]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.
favoritebooleanControlled star state. Setting it also shows the star.
defaultFavoritedefaultfalsebooleanThe 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.
showFavoritedefaulttruebooleanThe star after the title (a FavoriteButton with a Star in the tone).
followingbooleanControlled follow state. Setting it also shows the toggle.
defaultFollowingdefaultfalsebooleanThe 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.
showFollowdefaultfalsebooleanA 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.
pagerEntityPagerPrevious / 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.
previousShortcutdefault""stringA 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.
nextShortcutdefault""stringA key for the next record ("j" in the demo).
breadcrumbBreadcrumbEntry[] | ReactNodepage-header's trail above the title; the pager sits at the end of its row.
backHrefdefault""stringA back button before the title (a ‹ link on phones, where it replaces the breadcrumb; the pager stays).
onBack() => voidRuns when the back button is pressed (renders it without backHref).
tabsReactNodeNavTabs (or a Tabs list) on the header's bottom border.
metaPageHeaderMeta[] | ReactNodepage-header's small wrapping meta row under the description; facts are the richer choice for records.
childrenReactNodeExtra content after the facts, before the tabs (a progress bar, a notice).
stickydefault"condensed""none" | "condensed"condensed: once the header scrolls away a glass bar with the avatar, the name and the primary action pins under the top bar.
maxActionsdefault0numberThe 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.
mobileActionsdefault"menu""menu" | "stack"The compact layout (header under 512px): secondary actions and the follow toggle in the ⋯ action sheet, or stacked full width.
borderdefaulttruebooleanThe hairline under the header.
sizedefault"md""sm" | "md" | "lg"page-header's sizes (18 / 24 / 32px titles, 18 / 22 / 26px on phones) with fact values at 13 / 14 / 15px.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"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.
colordefault""stringAny CSS color for the avatar / icon tile and the star; replaces the tone.
loadingdefaultfalsebooleanpage-header's skeletons plus skeleton facts in the same layout (4 while facts is still an empty array).
localedefault""stringFormats the pager count and the Show all count ("1,234 of 5,000" in en-US). Empty follows the UIProvider, then the browser.
labelsPartial<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.
scrollTargetHTMLElement | Window | RefObject<HTMLElement | null> | nullWhat scrolls, for the condensed bar. Default: the nearest ScrollContainerProvider, else the nearest scrolling parent, else the window.
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for the trail, the actions, fact links and pager hrefs. Default: the LinkProvider's.
classNamesPartial<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.
pageHeaderClassNamesPartial<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.
classNamestringClasses 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
componentThe 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
constantThe 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.
Dependencies
Files
- components/ui/entity-header.tsx
FAQ
Is the React Entity Header component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Entity Header?
Run npx wingo-ui@latest add entity-header. It also adds the Wingo UI items it builds on (Badge, Breadcrumb, Button, Copy Button, Favorite button and 14 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/entity-header.
How does Entity Header behave on phones?
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.