Page title
The document page body and its one h1 with description and actions.
Payment runs
Approved invoices are batched into runs and paid on the day you choose. Hold or move a run up to its cutoff.
import { Button } from "@oration/canon/components/button";import { PageTitle } from "@oration/canon/components/page-title";import { toast } from "@oration/canon/components/toast";import { CalendarPlusIcon, DownloadIcon } from "lucide-react";export function Hero() { return ( <div className="w-full max-w-3xl text-left"> <PageTitle title="Payment runs" description="Approved invoices are batched into runs and paid on the day you choose. Hold or move a run up to its cutoff." actions={ <> <Button type="button" variant="outline" size="sm" onClick={() => toast.add({ title: "Export started", description: "We'll email the CSV to maya@cedarline.com.", }) } > <DownloadIcon data-icon="inline-start" aria-hidden="true" /> Export CSV </Button> <Button type="button" size="sm" onClick={() => toast.add({ type: "success", title: "Payment run scheduled", description: "Friday, Oct 2 at 2:00 PM CT.", }) } > <CalendarPlusIcon data-icon="inline-start" aria-hidden="true" /> Schedule run </Button> </> } /> </div> );}Usage#
Page title is the page's one h1, with an optional sentence of description, a leading icon or avatar, and the page's actions on the trailing side. PageBody is the centered 72rem column that document pages sit in. What goes wrong is the heading outline: a page has exactly one h1, so a page without a visible page title passes heading to the app header instead, and nothing else on the page is an h1.
When to use
- At the top of every document page: list pages, settings pages, record pages and overviews.
- To hold the page's main action, the one filled button, with its secondary actions beside it.
- With an avatar or monogram tile in
iconon a record page, such as a supplier or a customer. PageBodyas the content column of a document page, so pages share one width and one gutter.
When not to use
- For a heading inside the page. Sections get an
h2. Use Section header - For a block of settings on a settings page. Use Settings section
- For where-am-I navigation at the top of the window. Use Breadcrumb
- For the title of a dialog or sheet. Use Dialog
- For full-bleed tool screens such as the inbox or a flow canvas, which name themselves in the header. Use Three-pane workspace
One h1 per page
h1. It is the page title, or, where the design has no visible title, the visually hidden heading of the app header. Sections are h2.The One Filled Button Rule
sm in the product.The Quiet Indigo Rule
Anatomy#
Northwind
Paid by ACH on net 30.
- Icon. Optional. An avatar or monogram tile on the leading side, top-aligned with the title, 12px from it.
- Title. The
h1: 20px semibold with -1.5% tracking. Wraps rather than truncating. - Description. Optional. One 14px muted sentence, up to 42rem wide.
- Actions. Optional buttons on the trailing side, top-aligned. They wrap under the title block when there isn't room.
- Page body.
PageBody: a centered column, 72rem wide at most, with 24px side and 32px top and bottom padding.
Examples#
Record page with an icon
A record page leads with the record's tile or avatar in icon and can carry one status tag in the title. Its actions are the record's, with overflow in a ghost icon button.
Orchard Street FoodsOn hold
Payments paused until a new W-9 is on file. Last paid Friday, Sep 25.
import { Button } from "@oration/canon/components/button";import { MonogramTile } from "@oration/canon/components/monogram-tile";import { PageTitle } from "@oration/canon/components/page-title";import { Tag } from "@oration/canon/components/tag";import { toast } from "@oration/canon/components/toast";import { Tooltip, TooltipContent, TooltipTrigger } from "@oration/canon/components/tooltip";import { MailIcon, MoreHorizontalIcon } from "lucide-react";export function WithIcon() { return ( <div className="w-full max-w-3xl"> <PageTitle icon={ <MonogramTile name="Orchard Street Foods" color="green" size="xl" className="shrink-0" /> } title={ <span className="flex flex-wrap items-center gap-2"> Orchard Street Foods <Tag color="amber">On hold</Tag> </span> } description="Payments paused until a new W-9 is on file. Last paid Friday, Sep 25." actions={ <> <Button type="button" variant="outline" size="sm" onClick={() => toast.add({ title: "W-9 request sent", description: "Sent to accounts@orchardst.com.", }) } > <MailIcon data-icon="inline-start" aria-hidden="true" /> Request W-9 </Button> <Tooltip> <TooltipTrigger render={ <Button type="button" variant="ghost" size="icon-sm" aria-label="More supplier actions" onClick={() => toast.add({ title: "Supplier menu opened", }) } /> } > <MoreHorizontalIcon aria-hidden="true" /> </TooltipTrigger> <TooltipContent> More supplier actions </TooltipContent> </Tooltip> </> } /> </div> );}Actions wrap on narrow screens
The title block keeps at least 16rem, so in a narrow column the actions drop under it instead of squeezing the title.
Remittances
Every remittance advice sent to suppliers, with delivery status.
import { Button } from "@oration/canon/components/button";import { PageTitle } from "@oration/canon/components/page-title";import { toast } from "@oration/canon/components/toast";export function Wrapping() { return ( <div className="w-full max-w-sm rounded-xl bg-background p-4 shadow-border"> <PageTitle title="Remittances" description="Every remittance advice sent to suppliers, with delivery status." actions={ <> <Button type="button" variant="outline" size="sm" onClick={() => toast.add({ title: "Export started" }) } > Export CSV </Button> <Button type="button" size="sm" onClick={() => toast.add({ title: "Remittances resent" }) } > Resend failed </Button> </> } /> </div> );}In a page body
PageBody centers the page column and sets its gutters. A settings page puts 40px between the title and its first section.
Remittance
What suppliers receive when you pay them, and where their replies go.
Delivery
Sent after each payment run settles.
import { PageBody, PageTitle } from "@oration/canon/components/page-title";import { SettingsGroup, SettingsRow, SettingsSection } from "@oration/canon/components/settings-section";import { Switch } from "@oration/canon/components/switch";import { toast } from "@oration/canon/components/toast";export function InPageBody() { return ( <div className="w-full overflow-hidden rounded-xl bg-background shadow-border"> <PageBody className="max-w-3xl"> <PageTitle title="Remittance" description="What suppliers receive when you pay them, and where their replies go." /> <div className="mt-10"> <SettingsSection title="Delivery" description="Sent after each payment run settles." > <SettingsGroup> <SettingsRow inline label="Attach invoice copies" description="Adds each paid invoice as a PDF." > <Switch defaultChecked aria-label="Attach invoice copies" onCheckedChange={(on) => toast.add({ title: on ? "Invoice copies attached" : "Invoice copies removed", }) } /> </SettingsRow> </SettingsGroup> </SettingsSection> </div> </PageBody> </div> );}States#
| State | Treatment |
|---|---|
| Rest | Title block on the leading side, actions on the trailing side. |
| Wrapped | When the row is narrower than the 16rem title block plus the actions, the actions move below it, aligned to the start. |
| With icon | The icon sits beside the title and description, aligned to the top. |
| Title only | Without description or actions it is a single 20px line. |
Behavior#
- The row is a wrapping flex: the title block grows from a 16rem basis and the actions don't shrink, so actions drop below the title on narrow screens instead of squeezing it.
- PageTitle has no outer margin. Space it from what follows at the call site; settings pages put 40px before the first section, list pages use
className="mb-6". PageBodyspreads its props onto a<div>withmx-auto w-full max-w-6xl px-6 py-8; override the width withclassNamefor full-width lists.- Neither component has state or effects, so both render on the server.
- Pages that don't show a page title pass
headingto the app header, which renders a visually hiddenh1.
Do and don't#
Suppliers
Suppliers
Callbacks
Calls you and the team promised to return, including ones the voice agent scheduled.
Manage your callbacks here
This page lets you manage your callbacks.
h2 section headings under it.Content#
- List pages are a plural noun: Suppliers, Payment runs, Callbacks. Record pages are the record's name: Northwind Freight.
- Settings pages are the area's name: Remittance, Business hours.
- The description is one sentence on what's here and its scope: Every supplier contact who has reached Cedarline support, with their open conversations.
- Keep counts and status out of the title text. Put status in a tag beside it and counts in the content.
- Action labels are verb first: Schedule run, Import contacts, Export CSV.
Accessibility#
- The title is the page's
h1. Check the heading outline: oneh1, thenh2for sections. - An avatar in
iconthat repeats the title is decorative; hide it from assistive tech or give it empty alt text. - Actions come after the title and description in source order, so they are read after the page's name.
- Don't put links or buttons inside
title. Keep the heading plain text, plus at most a tag. - Icon-only actions need an
aria-labeland a tooltip.
Design tokens#
| Token | Used for |
|---|---|
--foreground | Title |
--muted-foreground | Description |
text-xl | 20px title |
tracking-[-0.015em] | Title tracking |
max-w-6xl | PageBody width (72rem) |
px-6 py-8 | PageBody gutters |
API reference#
PageTitle
The page's h1 row. Takes no other props; className lands on the row.
| Prop | Type | Default | Description |
|---|---|---|---|
titleRequired | React.ReactNode | No default | The h1 content. Text, plus at most a tag. |
description | React.ReactNode | No default | One sentence under the title. Renders in a <p>. |
actions | React.ReactNode | No default | Buttons on the trailing side. |
icon | React.ReactNode | No default | An avatar or tile on the leading side. |
className | string | No default | Classes for the row, such as a bottom margin. |
PageBody
The document page's content column.
Other props spread onto <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged after the column classes. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
PageTitle always renders an h1 and takes no id or heading level, so it can't be previewed or reused without adding another h1. The previews on this page add several to it.
description renders inside a <p>, so block content there produces invalid HTML.
There is no spacing contract below the title. Call sites add mb-6, mt-10 on the next block or nothing, so the gap varies from page to page.