Shade — page templates
When building a new admin page, the first question is what page type is this? — then reach for the matching pattern instead of inventing chrome.
Page type taxonomy
List page
Browsing or scanning a collection of items.
- Live examples: Members, Tags, Comments, Automations, ActivityPub.
- Chrome shape:
PageHeader(title + count + search/filter/actions) → table or list → empty state → pagination. - Pattern:
ListPage(composesPageHeader). Heavy filter UI usesFilters.
Detail page
Working with a single item — viewing, editing, or both.
- Live examples: Post editor.
- Chrome shape:
PageHeaderwith breadcrumb + title + meta, a primary action area, and a content area dedicated to the item. - Pattern: not built this milestone. Use
PageHeaderdirectly and lay out the body yourself.
Settings
Out of scope for the current page-template milestone. Don't force a settings page into ListPage or PageHeader.
Workflow / multi-step flows
No standard yet — too few examples. Don't standardise prematurely.
Canonical list page skeleton
import {ListPage} from '@tryghost/shade/page-templates';
import {PageHeader, ViewBar, FilterBar} from '@tryghost/shade/patterns';
import {Button, EmptyIndicator, Table} from '@tryghost/shade/components';
<ListPage>
<ListPage.Header>
{/* sticky={false} — ListPage.Header owns stickiness and blur */}
<PageHeader sticky={false} blurredBackground={false}>
<PageHeader.Left>
<PageHeader.Title>
Members<PageHeader.Count>{count}</PageHeader.Count>
</PageHeader.Title>
</PageHeader.Left>
<PageHeader.Actions>
<PageHeader.ActionGroup>
<Button>Add member</Button>
</PageHeader.ActionGroup>
</PageHeader.Actions>
</PageHeader>
<ViewBar>{/* optional */}</ViewBar>
<FilterBar>{/* optional — auto-collapses when empty */}</FilterBar>
</ListPage.Header>
<ListPage.Body>
{items.length === 0 ? <EmptyIndicator title='No members yet' /> : <Table>...</Table>}
</ListPage.Body>
</ListPage>
Gotchas
sticky={false}onPageHeaderinsideListPage.Header. The wrapper handles stickiness and blur — leavingPageHeadersticky stacks two sticky containers and breaks scroll.- Don't put
useQueryinside a pattern. State lives in the consumer. Patterns are layout/composition contracts. PageHeaderis slot-based (.Left,.Title,.Count,.Description,.Meta,.Actions,.ActionGroup,.Breadcrumb) — don't pass a prop bag.FilterBarauto-collapses when empty — render it unconditionally; no need to conditionally mount.
Subcomponent inventory (PageHeader)
PageHeader.Left— title block containerPageHeader.Breadcrumb— small muted breadcrumb above the titlePageHeader.Title— H1, accepts inlineCountPageHeader.Count— inline secondary count next to the titlePageHeader.Description— paragraph below the titlePageHeader.Meta— small muted metadata line belowPageHeader.Actions— right-side action areaPageHeader.ActionGroup— grouping for buttons
When ListPage doesn't fit
- The page is a Detail page → use
PageHeaderdirectly; build the body from primitives + components. - The page is Settings → out of scope; use whatever the surrounding settings shell uses.
- The page is a multi-step flow → no pattern yet; assemble from primitives.
If you're tempted to force a non-list shape into ListPage, stop and check whether you're actually building one of those three other shapes.
Source of truth
Storybook → Page Templates / Page Types and the ListPage and PageHeader
stories.