Layout
Page header
The top of a page: title, description, breadcrumb, facts, actions and tabs, reflowing in narrow space.
- Built on
<header>Container queries- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.9 kB
- Dependencies
- React
Invoices
Try its options
Loading its options.
Code
import { Button } from "@/components/equip/button";
import { PageHeader } from "@/components/equip/page-header";
export default function Demo() {
return (
<PageHeader
level={2}
title="Invoices"
meta={[<span key="count">142 this quarter</span>, <span key="due">3 overdue</span>]}
primaryAction={
<Button size="s" variant="accent">
New invoice
</Button>
}
secondaryActions={
<Button size="s" variant="outline">
Export
</Button>
}
style={{ width: 208 }}
/>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<header style="width:208px" class="eq-page-header">
<div class="eq-page-header-layout">
<div class="eq-page-header-main">
<div class="eq-page-header-heading">
<h2 class="eq-page-header-title">Invoices</h2>
<ul class="eq-page-header-meta">
<li class="eq-page-header-meta-item">
<span>142 this quarter</span>
</li>
<li class="eq-page-header-meta-item">
<span>3 overdue</span>
</li>
</ul>
</div>
<div class="eq-page-header-actions">
<button type="button" class="eq-button" data-variant="outline" data-size="s">
<span class="eq-button-label">Export</span>
</button>
<button type="button" class="eq-button" data-variant="accent" data-size="s">
<span class="eq-button-label">New invoice</span>
</button>
</div>
</div>
</div>
</header>Examples
Everything in place
Below 30rem of its own width the title steps down and the actions take a full row, so the header reflows inside a narrow panel as well as on a phone.
larder-storefront
The customer storefront and checkout, deployed from main on every merge.
Code
import { Badge } from "@/components/equip/badge";
import { Breadcrumb } from "@/components/equip/breadcrumb";
import { Button } from "@/components/equip/button";
import { Tab, TabList, TabPanel, Tabs } from "@/components/equip/tabs";
import { PageHeader } from "@/components/equip/page-header";
export default function Example() {
return (
<Tabs defaultValue="deployments" style={{ width: "100%" }}>
<PageHeader
level={2}
breadcrumb={
<Breadcrumb
items={[
{ label: "Acme", href: "#acme" },
{ label: "Projects", href: "#projects" },
{ label: "larder-storefront" },
]}
/>
}
title="larder-storefront"
description="The customer storefront and checkout, deployed from main on every merge."
meta={[
<Badge key="status" tone="ok" dot>
Healthy
</Badge>,
<span key="region">eu-west-1</span>,
<span key="deploy">Deployed 12 min ago by Mira Holt</span>,
]}
secondaryActions={
<Button variant="outline" size="s">
Visit site
</Button>
}
primaryAction={
<Button variant="accent" size="s">
Deploy
</Button>
}
tabs={
<TabList aria-label="Project">
<Tab value="deployments">Deployments</Tab>
<Tab value="logs">Logs</Tab>
<Tab value="settings">Settings</Tab>
</TabList>
}
/>
<TabPanel value="deployments">48 deployments this month, the last 3 promoted to production.</TabPanel>
<TabPanel value="logs">Streaming logs from 4 instances.</TabPanel>
<TabPanel value="settings">Build command, environment variables and domains.</TabPanel>
</Tabs>
);
}Title and one action
Every slot is optional; with only a title and an action the header is a single row.
Team members
People who can see and edit projects in Larder.
Code
import { Button } from "@/components/equip/button";
import { PageHeader } from "@/components/equip/page-header";
export default function Example() {
return (
<PageHeader
level={2}
title="Team members"
description="People who can see and edit projects in Larder."
primaryAction={<Button size="s">Invite people</Button>}
style={{ width: "100%" }}
/>
);
}HTMLworks in any framework, with the part's CSS
<header style="width:100%" class="eq-page-header">
<div class="eq-page-header-layout">
<div class="eq-page-header-main">
<div class="eq-page-header-heading">
<h2 class="eq-page-header-title">Team members</h2>
<p class="eq-page-header-description">People who can see and edit projects in Larder.</p>
</div>
<div class="eq-page-header-actions">
<button type="button" class="eq-button" data-variant="solid" data-size="s">
<span class="eq-button-label">Invite people</span>
</button>
</div>
</div>
</div>
</header>Display size
size="l" sets the title from 28px to 40px by the header's own width, for a detail page whose title is the thing people came for.
Order 2041 to Natoora
Seventeen lines, sent at 14:05 for delivery before 07:00 tomorrow.
Code
import { Button } from "@/components/equip/button";
import { PageHeader } from "@/components/equip/page-header";
export default function Example() {
return (
<PageHeader
level={2}
size="l"
style={{ width: "100%" }}
title="Order 2041 to Natoora"
description="Seventeen lines, sent at 14:05 for delivery before 07:00 tomorrow."
meta={[<span key="status">Sent</span>, <span key="total">£612.40</span>, <span key="by">By Ines Duarte</span>]}
secondaryActions={<Button variant="outline">Duplicate</Button>}
primaryAction={<Button variant="accent">Check in delivery</Button>}
/>
);
}HTMLworks in any framework, with the part's CSS
<header style="width:100%" class="eq-page-header" data-size="l">
<div class="eq-page-header-layout">
<div class="eq-page-header-main">
<div class="eq-page-header-heading">
<h2 class="eq-page-header-title">Order 2041 to Natoora</h2>
<p class="eq-page-header-description">Seventeen lines, sent at 14:05 for delivery before 07:00 tomorrow.</p>
<ul class="eq-page-header-meta">
<li class="eq-page-header-meta-item">
<span>Sent</span>
</li>
<li class="eq-page-header-meta-item">
<span>£612.40</span>
</li>
<li class="eq-page-header-meta-item">
<span>By Ines Duarte</span>
</li>
</ul>
</div>
<div class="eq-page-header-actions">
<button type="button" class="eq-button" data-variant="outline" data-size="m">
<span class="eq-button-label">Duplicate</span>
</button>
<button type="button" class="eq-button" data-variant="accent" data-size="m">
<span class="eq-button-label">Check in delivery</span>
</button>
</div>
</div>
</div>
</header>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title | ReactNode | none | The page title. |
description | ReactNode | none | One or two sentences under the title. |
breadcrumb | ReactNode | none | A Breadcrumb or back link above the title. |
meta | ReactNode | none | Short facts such as status, owner and date. Each child becomes an item in a list. |
primaryAction | ReactNode | none | The one main action, placed at the end of the row. |
secondaryActions | ReactNode | none | Quieter actions, placed before the primary one. |
tabs | ReactNode | none | A TabList or other sub navigation along the bottom. |
level | 1 | 2 | 1 | The heading element. 1 for the page title. |
size | "m" | "l" | "m" | m for an app page. l is a display title that grows from 28px to 40px with the header's width, for a detail or landing page that is mostly this one thing. |
...rest | ComponentProps<"header"> | none | Every native header attribute. |
Anatomy
| root | .eq-page-header | The header and the size container its layout queries. data-size="l" for the display title. |
|---|---|---|
| layout | .eq-page-header-layout | |
| breadcrumb | .eq-page-header-breadcrumb | |
| main row | .eq-page-header-main | Heading and actions; the actions wrap below when there is no room. |
| heading | .eq-page-header-heading | |
| title | .eq-page-header-title | |
| description | .eq-page-header-description | |
| actions | .eq-page-header-actions | |
| meta | .eq-page-header-meta | A ul. |
| meta item | .eq-page-header-meta-item | |
| tabs | .eq-page-header-tabs | Scrolls sideways when narrow instead of widening the page. |
Notes
- Renders on the server with no client JavaScript.
- Below 30rem of its own width the title steps down a size and the actions take a full row, each growing to share it. A size l title keeps its own 28px floor there instead.
- The primary action comes last in the DOM as well as on screen, so Tab reaches the quieter actions first and the order matches what people see.
- Inside main a header element is not a banner landmark, which is right for a page header; the h1 is what people navigate by.
- The meta facts are a list, so assistive tech announces how many there are.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/page-header.tsx74 lines
import { Children, type ComponentProps, type ReactNode } from "react";
import "./page-header.css";
export type PageHeaderProps = Omit<ComponentProps<"header">, "title"> & {
title: ReactNode;
/** One or two sentences under the title. */
description?: ReactNode;
/** A Breadcrumb, or a back link, above the title. */
breadcrumb?: ReactNode;
/** Short facts under the title, such as status, owner and date. Each child becomes one item. */
meta?: ReactNode;
/** The one main action, placed last so it sits at the end of the row. */
primaryAction?: ReactNode;
/** Quieter actions, placed before the primary one. */
secondaryActions?: ReactNode;
/** A Tabs list or other sub navigation along the bottom edge. */
tabs?: ReactNode;
/** The heading element. 1 for the page title. */
level?: 1 | 2;
/** m for an app page; l is a display title that scales with the header's width, for a page that is mostly this one thing. */
size?: "m" | "l";
};
export function PageHeader({
title,
description,
breadcrumb,
meta,
primaryAction,
secondaryActions,
tabs,
level = 1,
size = "m",
className,
...rest
}: PageHeaderProps) {
const Heading = `h${level}` as "h1";
const hasActions = primaryAction != null || secondaryActions != null;
const metaItems = Children.toArray(meta);
return (
<header
{...rest}
className={["eq-page-header", className].filter(Boolean).join(" ")}
data-size={size === "m" ? undefined : size}
>
<div className="eq-page-header-layout">
{breadcrumb != null && <div className="eq-page-header-breadcrumb">{breadcrumb}</div>}
<div className="eq-page-header-main">
<div className="eq-page-header-heading">
<Heading className="eq-page-header-title">{title}</Heading>
{description != null && <p className="eq-page-header-description">{description}</p>}
{metaItems.length > 0 && (
<ul className="eq-page-header-meta">
{metaItems.map((item, index) => (
<li key={index} className="eq-page-header-meta-item">
{item}
</li>
))}
</ul>
)}
</div>
{hasActions && (
<div className="eq-page-header-actions">
{secondaryActions}
{primaryAction}
</div>
)}
</div>
{tabs != null && <div className="eq-page-header-tabs">{tabs}</div>}
</div>
</header>
);
}components/equip/page-header.css120 lines
@layer equip {
.eq-page-header {
box-sizing: border-box;
display: block;
min-width: 0;
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-m) / 1.5 var(--eq-font);
/* The header is the container; its layout answers to its own width, not the viewport. */
container: eq-page-header / inline-size;
}
.eq-page-header-layout {
display: flex;
flex-direction: column;
gap: var(--eq-space-3);
}
.eq-page-header-breadcrumb {
min-width: 0;
}
.eq-page-header-main {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: var(--eq-space-3) var(--eq-space-5);
}
.eq-page-header-heading {
flex: 1 1 18rem;
min-width: 0;
}
.eq-page-header-title {
margin: 0;
font-size: calc(var(--eq-text-l) * 1.5);
font-weight: var(--eq-weight-strong);
line-height: 1.2;
letter-spacing: -0.016em;
overflow-wrap: anywhere;
text-wrap: balance;
}
/* Large: a display title from 28px to 40px, following the header's own width at every size. */
.eq-page-header[data-size="l"] .eq-page-header-title {
font-size: clamp(calc(var(--eq-text-l) * 1.75), 5.5cqi, calc(var(--eq-text-l) * 2.5));
line-height: 1.08;
letter-spacing: -0.025em;
}
.eq-page-header-description {
max-width: 65ch;
margin: var(--eq-space-1) 0 0;
color: var(--eq-ink-2);
text-wrap: pretty;
}
.eq-page-header-actions {
flex: none;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--eq-space-2);
}
.eq-page-header-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--eq-space-1) var(--eq-space-4);
margin: var(--eq-space-2) 0 0;
padding: 0;
list-style: none;
color: var(--eq-ink-2);
font-size: var(--eq-text-s);
}
.eq-page-header-meta-item {
display: inline-flex;
align-items: center;
gap: var(--eq-space-1);
min-width: 0;
}
.eq-page-header-meta-item > svg {
flex: none;
width: 14px;
height: 14px;
}
.eq-page-header-tabs {
min-width: 0;
margin-top: var(--eq-space-1);
}
/* Narrow: a smaller title, and the actions take the whole row so each is easy to reach. */
@container eq-page-header (width < 30rem) {
.eq-page-header-title {
font-size: calc(var(--eq-text-l) * 1.25);
}
.eq-page-header-actions {
flex: 1 1 100%;
}
.eq-page-header-actions > * {
flex: 1 1 auto;
}
/* Tabs scroll sideways rather than push the page wider. The padding, taken back by the
negative margin, leaves room for the focus ring inside the scroller's clip. */
.eq-page-header-tabs {
margin: 0 calc(-1 * var(--eq-space-1)) calc(-1 * var(--eq-space-1));
padding: var(--eq-space-1);
overflow-x: auto;
scrollbar-width: none;
}
}
}