Skip to content

Page title

The document page body and its one h1 with description and actions.

Status
Beta
Category
Layout
Adoption
Not used yet
import { PageTitle } from "@oration/canon/components/page-title";
packages/canon/src/components/page-title.tsx

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 icon on a record page, such as a supplier or a customer.
  • PageBody as 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

Every page has exactly one 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

Page actions hold the view's one filled button at most, last in the row. Everything else beside it is outline or ghost, at sm in the product.

The Quiet Indigo Rule

Titles are ink, never indigo or a category hue. Status beside a title is a tag with words, not a colored title.

Anatomy#

Northwind

Paid by ACH on net 30.

  1. Icon. Optional. An avatar or monogram tile on the leading side, top-aligned with the title, 12px from it.
  2. Title. The h1: 20px semibold with -1.5% tracking. Wraps rather than truncating.
  3. Description. Optional. One 14px muted sentence, up to 42rem wide.
  4. Actions. Optional buttons on the trailing side, top-aligned. They wrap under the title block when there isn't room.
  5. 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.

Attach invoice copies
Adds each paid invoice as a PDF.
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#

States
StateTreatment
RestTitle block on the leading side, actions on the trailing side.
WrappedWhen the row is narrower than the 16rem title block plus the actions, the actions move below it, aligned to the start.
With iconThe icon sits beside the title and description, aligned to the top.
Title onlyWithout 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".
  • PageBody spreads its props onto a <div> with mx-auto w-full max-w-6xl px-6 py-8; override the width with className for full-width lists.
  • Neither component has state or effects, so both render on the server.
  • Pages that don't show a page title pass heading to the app header, which renders a visually hidden h1.

Do and don't#

Suppliers

Do. Give the page one filled action, last, with secondary actions as outline.

Suppliers

Don't. Fill every action in the title row. Nothing reads as the main thing to do.

Callbacks

Calls you and the team promised to return, including ones the voice agent scheduled.

Do. Name the place in the title and say what's in it in the description.

Manage your callbacks here

This page lets you manage your callbacks.

Don't. Write the title as an instruction and repeat it in the description.
Do. Use one page title per page and h2 section headings under it.
Don't. Add a second page title for a tab or a section. Screen reader users lose the page's name.

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: one h1, then h2 for sections.
  • An avatar in icon that 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-label and a tooltip.

Design tokens#

Design tokens
TokenUsed for
--foregroundTitle
--muted-foregroundDescription
text-xl20px title
tracking-[-0.015em]Title tracking
max-w-6xlPageBody width (72rem)
px-6 py-8PageBody gutters

API reference#

PageTitle

The page's h1 row. Takes no other props; className lands on the row.

Props of PageTitle
PropTypeDefaultDescription
titleRequiredReact.ReactNodeNo defaultThe h1 content. Text, plus at most a tag.
descriptionReact.ReactNodeNo defaultOne sentence under the title. Renders in a <p>.
actionsReact.ReactNodeNo defaultButtons on the trailing side.
iconReact.ReactNodeNo defaultAn avatar or tile on the leading side.
classNamestringNo defaultClasses for the row, such as a bottom margin.

PageBody

The document page's content column.

Other props spread onto <div>.

Props of PageBody
PropTypeDefaultDescription
classNamestringNo defaultMerged 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.