Patterns
Page
The column, the title block and the sections that give each page the same structure.
Patterns
The column, the title block and the sections that give each page the same structure.
They can sign in today.
An invitation stays valid for 7 days.
import { Badge, Button, Card, CardContent, PageContainer, PageHeader, PageSection } from 'ferry-ui'
import { UserPlus } from 'lucide-react'
const MEMBERS = [
{ name: 'Maya Chen', email: 'maya@example.com', role: 'Owner' },
{ name: 'Jonas Weber', email: 'jonas@example.com', role: 'Admin' },
{ name: 'Priya Patel', email: 'priya@example.com', role: 'Member' },
]
export default function PageHero() {
return (
<div className="rounded-lg border">
<PageContainer size="narrow">
<PageHeader
title="Members"
description="People with access to this workspace."
actions={
<Button variant="primary" icon={<UserPlus />}>
Invite member
</Button>
}
/>
<PageSection title="Active members" description="They can sign in today.">
<Card className="divide-y">
{MEMBERS.map((member) => (
<CardContent key={member.email} className="flex items-center justify-between gap-4">
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm text-foreground">{member.name}</span>
<span className="truncate text-[13px] text-foreground-light">{member.email}</span>
</div>
<Badge>{member.role}</Badge>
</CardContent>
))}
</Card>
</PageSection>
<PageSection title="Pending invitations" description="An invitation stays valid for 7 days.">
<Card>
<CardContent className="text-[13px] text-foreground-light">No invitation is pending.</CardContent>
</Card>
</PageSection>
</PageContainer>
</div>
)
}PageContainer in the page region of App Shell. Do not nest containers.PageHeader is the first child of the container. It holds the only <h1> of the page.PageSection divides the page into topics, such as "General" and "Billing". It does not group the rows of a card.PageBackLink points to the parent page. For a deeper hierarchy, use a Breadcrumb.Import the parts and put them together.
import { PageBackLink, PageContainer, PageHeader, PageSection } from 'ferry-ui'
<PageContainer>
<PageHeader title="" eyebrow={<PageBackLink href="" />} />
<PageSection title="" />
<PageSection title="" />
</PageContainer>| Part | Element | Role |
|---|---|---|
PageContainer | div | The centered column. It sets the maximum width and the padding. |
PageHeader | header | The title block. It holds the h1 of the page. |
PageBackLink | a | The link to the parent page. It goes in eyebrow. |
PageSection | section | One topic of the page. Its title is an h2. |
Give the buttons of the page to actions. Put the secondary buttons first and the primary button last.
import { Button, PageHeader } from 'ferry-ui'
import { Download, Plus } from 'lucide-react'
export default function PageHeaderActions() {
return (
<PageHeader
title="Invoices"
description="Each invoice of your customers, newest first."
actions={
<>
<Button icon={<Download />}>Export</Button>
<Button variant="primary" icon={<Plus />}>
New invoice
</Button>
</>
}
/>
)
}badges shows small elements after the title. Use it for a tag or for the status of a record.
import { Badge, PageHeader } from 'ferry-ui'
export default function PageHeaderBadges() {
return (
<PageHeader
title="Usage reports"
description="The API calls of each project, day by day."
badges={<Badge variant="info">Beta</Badge>}
/>
)
}On a detail page or a create page, put a PageBackLink in eyebrow. Write only the name of the parent page. The component adds the arrow.
import type * as React from 'react'
import { PageBackLink, PageHeader } from 'ferry-ui'
// The demo stays on this page. In an app, give the path to `href` and remove `onClick`.
const stay = (event: React.MouseEvent) => event.preventDefault()
export default function PageBackLinkDemo() {
return (
<PageHeader
eyebrow={
<PageBackLink href="#" onClick={stay}>
Projects
</PageBackLink>
}
title="Create a project"
description="Give the project a name. You can change it later in the settings."
/>
)
}Set size="lg" on the home page of one record. The title and the description become larger.
import type * as React from 'react'
import { Button, PageBackLink, PageHeader, StatusBadge } from 'ferry-ui'
import { Download } from 'lucide-react'
// The demo stays on this page. In an app, give the path to `href` and remove `onClick`.
const stay = (event: React.MouseEvent) => event.preventDefault()
export default function PageHeaderLarge() {
return (
<PageHeader
size="lg"
eyebrow={
<PageBackLink href="#" onClick={stay}>
Customers
</PageBackLink>
}
title="Acme"
description="Customer since March 2024. 42 seats."
badges={<StatusBadge tone="success" label="Active" size="sm" />}
actions={<Button icon={<Download />}>Export</Button>}
/>
)
}The children of PageHeader show below the title row. Use them for Tabs or a Callout.
import { Button, PageHeader, Tabs, TabsList, TabsTrigger } from 'ferry-ui'
import { UserPlus } from 'lucide-react'
export default function PageHeaderTabs() {
return (
<PageHeader
title="Team"
description="The people who can open this workspace."
actions={
<Button variant="primary" icon={<UserPlus />}>
Invite member
</Button>
}
>
<Tabs defaultValue="members">
<TabsList aria-label="Team sections">
<TabsTrigger value="members">Members</TabsTrigger>
<TabsTrigger value="invitations">Invitations</TabsTrigger>
<TabsTrigger value="roles">Roles</TabsTrigger>
</TabsList>
</Tabs>
</PageHeader>
)
}PageSection takes a title, a description and actions. Pass an id to link to the section. The space between two sections is 40px.
The default card pays the invoice of each month.
import { Badge, Button, Card, CardContent, PageSection } from 'ferry-ui'
import { Plus } from 'lucide-react'
export default function PageSections() {
return (
<div>
<PageSection
id="payment-methods"
title="Payment methods"
description="The default card pays the invoice of each month."
actions={
<Button size="tiny" icon={<Plus />}>
Add card
</Button>
}
>
<Card>
<CardContent className="flex items-center justify-between gap-4 text-sm">
<span className="text-foreground">Card with the last digits 4242</span>
<Badge>Default</Badge>
</CardContent>
</Card>
</PageSection>
<PageSection id="billing-address" title="Billing address">
<Card>
<CardContent className="text-sm text-foreground">Acme, 12 Harbor Street, Portland</CardContent>
</Card>
</PageSection>
</div>
)
}The size prop of PageContainer sets the maximum width of the column.
<PageContainer size="narrow">
<PageHeader title="Project settings" />
</PageContainer>| Size | Content width | Use |
|---|---|---|
narrow | About 800px | Settings, forms, detail pages. |
default | About 1200px | Lists, overviews, dashboards. |
full | No maximum | Logs, wide tables, editors. |
PageHeader is the <h1> of the page. Do not add a second <h1>.PageSection is an <h2>. It also gives the section its accessible name.PageBackLink is decorative. The name of the parent page is the text of the link.Each part also accepts the attributes of its element.
| Prop | Type | Default |
|---|---|---|
size | "default" | "full" | "narrow" | default |
Max content width (default | ||
| Prop | Type | Default |
|---|---|---|
titleRequired | ReactNode | - |
Page title, rendered as the page's | ||
description | ReactNode | - |
One-sentence subtitle under the title (lighter foreground). | ||
badges | ReactNode | - |
Small badges next to the title (a | ||
actions | ReactNode | - |
Right-aligned toolbar: secondary buttons first, the page's primary action last. | ||
eyebrow | ReactNode | - |
Small line above the title (13px, lighter foreground): a | ||
size | "md" | "lg" | md |
| ||
children | ReactNode | - |
Extra content under the title row (tabs, a stat strip, a callout). | ||
PageBackLink uses the link component of Link Provider.
| Prop | Type | Default |
|---|---|---|
hrefRequired | string | - |
Destination: the parent page (usually the list this record belongs to). | ||
childrenRequired | ReactNode | - |
Name of the parent page, without an arrow or "Back to" ( | ||
linkComponent | LinkComponent | - |
Link component for this link only, overriding the one from the nearest | ||
| Prop | Type | Default |
|---|---|---|
title | ReactNode | - |
Section heading, rendered as an | ||
description | ReactNode | - |
One-sentence explanation under the heading. | ||
actions | ReactNode | - |
Compact controls aligned with the heading on the right (a "View all" link, an "Add" button). | ||