Using patterns
Patterns are blueprints for whole screens. Start every screen from one, and the layout, spacing and behavior come with it.
How Patterns Work
- A pattern lays out a screen. It places the header, toolbar, content and footer in the right spots, at every width.
- It is built only from kit components. It adds no new parts of its own.
- It keeps no data. Your screen owns the rows, the form values and what is selected, and passes them in.
- It has slots. Props like
header,toolbarandchildrentake kit components you fill in.
How to Use a Pattern
- Frame the page with AppShell when it is a full screen with the side navigation and top bar.
- Pick the page pattern that matches the screen, using the list below.
- Read its contract in
contracts/: the slots, the options and the do-nots. - Fill the slots with the kit components listed under “Built from”.
- Check it at every width with the Stage control on the pattern's page in this catalog.
- Missing something? Stop and decide: add the piece to the kit, or change the screen. Do not build a one-off.
Which One?
- AccountFlow — Prototyping account screens that link together: find an account, open it, add an account or a product.
- AppShell — Any product screen: a list, a record, a dashboard.
- ChatWindow — The assistant docked beside a product screen.
- Dashboard — A home screen that reports on a job: what is owed, what is due, what closed.
- FormPage — Making a new record from a page of its own, not from a Modal.
- GuidedProcessPage — A process of a few steps done in order where each step is a page of fields.
- ListDetail — Going through records one after another: an invoice queue, an inbox, approvals.
- ListPage — A screen that lists records with a search, filters and pages.
- RecordPage — One record with its details in sections.
- SettingsPage — The settings home: a page whose job is to send people on.
AccountFlow
The account provisioning flow: the accounts list, one account with its tabs, a new account, a new product on an account, and configuring that product alone or many products at once, one page at a time.
Use it for
- Prototyping account screens that link together: find an account, open it, add an account or a product.
- Showing how a list, a record and its create forms hand over to each other.
- Adding products to an account: pick one and configure it, or add many at once in a grid.
Built from
Avoid
- Do not put the list's New and Export in its Toolbar. Page actions go in the PageHeader; the Toolbar keeps filters, search, the view and refresh.
- Do not show the Account ID as plain text. It is a link Cell, so people can tell it opens the account.
- Do not use Inputs to show saved account values. Use FormDisplay.
Starting code
<AccountFlow
view={view}
list={<ListPage toolbar={toolbar} pagination={pagination}><Table columns={columns} rows={rows} /></ListPage>}
account={<RecordPage header={header} tabs={tabs} summary={scores}>{details}</RecordPage>}
newAccount={<FormPage header={newHeader}><Form id="account" columns={2} sections={sections} onSubmit={create} /></FormPage>}
newProduct={<FormPage header={productHeader}><Form id="product" columns={2} sections={productSections} onSubmit={add} /></FormPage>}
/>AppShell
The frame every product screen sits in: the bar across the top, the rail under it down the side, the page in the middle, and the assistant beside it.
Use it for
- Any product screen: a list, a record, a dashboard.
- A screen the assistant can be opened beside.
- The assistant's full-screen view, keeping the bar and the side nav.
Built from
Avoid
- Do not add state to it; it is a blueprint. Keep which nav section, which page, whether the assistant is open and whether the page header is compact in the screen.
- Do not build the bar, the rail or the page header inside it. Pass the kit pieces.
- Do not trim, reorder or invent side nav links. Every screen passes the full APP_NAV and APP_NAV_END from @/patterns/AppShell/appNav and marks its own page with current.
Starting code
<AppShell header={<AppHeader />} nav={<AppNav items={APP_NAV} endItems={APP_NAV_END} current={pageId} onNavigate={openPage} expanded={navOpen} />} pageHeader={<PageHeader title="Accounts" />}>{page}</AppShell>ChatWindow
The assistant window: its header over the turns, the composer at the foot, and the saved chats coming in beside or over them.
Use it for
- The assistant docked beside a product screen.
- The assistant's full-screen view, with the saved chats beside it.
Built from
Avoid
- Do not add state to it; it is a blueprint. Keep the view, the turns and the panel in the screen.
- Do not build the header, the turns or the box inside it. Pass the kit pieces.
- Do not build the Get Started block from EmptyState, Illustration and Buttons in the screen. Pass starters and art.
Starting code
<ChatWindow composer={<ChatComposer onSend={send} />} chatsCount={7} onClose={close}>{turns}</ChatWindow>Dashboard
The home screen of a job: the numbers that matter, grouped, with the charts and lists that explain them.
Use it for
- A home screen that reports on a job: what is owed, what is due, what closed.
- Numbers that need grouping, each group with its own charts and lists.
- A screen where people pick between saved dashboards.
Built from
Avoid
- Do not add state to it; it is a blueprint. Keep the dashboard, the open groups and the filters in the screen.
- Do not build the bar, the groups or the tiles inside it. Pass the kit pieces.
- Do not invent a tile grid, and do not put more than five tiles in a row. Two side by side is .layout-split, three is .layout-split--thirds, four or five is .layout-metrics with --fixed-4 or --fixed-5; read contracts/layout.json.
Starting code
<Dashboard label="Accounting dashboard" name="Accounting Dashboard" toolbar={<Toolbar views={saved} view={saved} />}>{groups}</Dashboard>FormPage
One record being filled in: its name and the save at the top, anything that has to be read first under that, and the fields in folding groups underneath.
Use it for
- Making a new record from a page of its own, not from a Modal.
- Editing a record's fields on a page, where the groups of fields are long enough to fold.
- The save has to be in reach while a long form is scrolled.
Built from
Avoid
- Do not invent CreatePage, EditPage, NewRecordPage or FormLayout.
- Do not put a Form inside every Section yourself. One Form holds the groups as sections, so one press saves the page.
- Do not add a second save bar at the foot. The header keeps the save in reach.
Starting code
<FormPage
columns={2}
header={<PageHeader breadcrumbs={[{ label: "Home", href: "/" }, { label: "Accounts", href: "/accounts" }]} title="Create account" actions={<><Button onClick={cancel}>Cancel</Button><Button emphasis="strong" intent="brand" type="submit" form="account">Create</Button></>} />}
notice={<Callout intent="info" onDismiss={hide}>Use this form for the account's general and default billing information.</Callout>}
>
<Form id="account" labelPosition="start" onSubmit={save} sections={sections} />
</FormPage>GuidedProcessPage
A process walked one page per step: a dark column per step before it starts, then each step's fields with every step on a dark panel at the start or end edge of the page.
Use it for
- A process of a few steps done in order where each step is a page of fields.
- Work that can be saved and picked up again part way through.
Built from
Avoid
- Do not invent WizardPage, StepPage, a steps panel or a footer bar of your own. The parts are GuidedProcess.
- Do not put a PageHeader over it. The header part names the step.
- Do not use it for a short flow; two or three fields belong in a Modal with Steps.
Starting code
<GuidedProcessPage
view="step" side="end" stepsOpen={open} onStepsClose={close}
header={<GuidedProcess part="header" title="Import usage" stepTitle="Map the columns" current={2} total={4} onStepsOpen={openSteps} />}
steps={<GuidedProcess part="steps" steps={steps} current={2} onClose={close} />}
footer={<GuidedProcess part="footer" updated="Last updated on September 13, 12:43 PM" actions={actions} onAction={handle} />}
>
<Form id="step" columns={2} onSubmit={next}>{fields}</Form>
</GuidedProcessPage>ListDetail
A list with the picked record beside it: a ListView on one side and the record in a pane on the other, or one at a time on a phone.
Use it for
- Going through records one after another: an invoice queue, an inbox, approvals.
- A record short enough to read in a pane beside the list.
- A list that must stay in view while its records are read.
- Records under other records, like an account's invoices: a nested ListView steps in, and the leaf opens beside it.
- A long queue split by status: a grouped ListView keeps what needs attention on top.
- Stacked when the list is short and the record needs the full width; inline when people read one record at a time and want the rest of the list to stay in place around it.
Built from
Avoid
- Do not add state to it; it is a blueprint. The screen keeps which record is open.
- Do not use it for records with a lot to show or edit. Drill into a RecordPage instead.
- Do not build the list inside it. Pass a kit ListView.
Starting code
<ListDetail
label="Invoices" open={Boolean(invoice)} onBack={() => setOpenId(null)} backLabel="Back to invoices"
title={invoice?.number} actions={<Button size="sm">Download</Button>}
list={<ListView label="Invoices" selection="single" interaction="drill" selected={openId ? [openId] : []} onOpen={setOpenId} items={invoices} />}
empty={<EmptyState icon="receipt_long" title="Pick an invoice" />}
detail={invoice && <Section title="Invoice Details">…</Section>}
/>ListPage
A list of records: the bar to find and filter them, the rows themselves, and the pages under them.
Use it for
- A screen that lists records with a search, filters and pages.
- A list that has to show loading, empty and failed states.
- A list people can lay out more than one way.
Built from
Avoid
- Do not add state to it; it is a blueprint. Keep the filters, the rows, the selection and the page in the screen.
- Do not build the bar, the rows or the pages inside it. Pass the kit pieces.
- Do not show the bulk bar and the toolbar at once; passing bulk swaps them.
Starting code
<ListPage label="Invoices" toolbar={<Toolbar />} pagination={<Pagination total={248} />}><Table columns={columns} rows={rows} /></ListPage>RecordPage
One record: its name and actions at the top, the tabs of what belongs to it, the numbers that matter, and its details in sections underneath.
Use it for
- One record with its details in sections.
- A record whose related lists live behind tabs.
Built from
Avoid
- Do not add state to it; it is a blueprint. Keep the tab, the sections and the notice in the screen.
- Do not build the header, the tabs or the bar inside it. Pass the kit pieces.
- Do not put page padding around it. It sits in the AppShell page, which owns that.
Starting code
<RecordPage label="Account" columns={2} header={<PageHeader title="Apex Digital Services" badge="Active" />} tabs={<Tabs items={parts} />} summary={<Scoreboard items={numbers} />}>
<Section title="Account information" collapsible>
<FormDisplay label="Account name" value="Apex Digital Services" />
<FormDisplay label="Account number" value="ACC-10428" />
</Section>
<Section title="Recent invoices"><Table columns={invoiceColumns} rows={invoices} /></Section>
</RecordPage>SettingsPage
The settings home: a grid of the places people can go from here, each with its own icon and color.
Use it for
- The settings home: a page whose job is to send people on.
- Another home shaped the same way, like a reports home.
- A set of places where the icon and its color help people find the same one again.
Built from
Avoid
- Do not add state to it; it is a blueprint.
- Do not build the tiles inside it. Pass kit Tiles.
- Do not use kit Cards as the tiles; a Card is for comparing and picking, a NavTile is a way in.
Starting code
<SettingsPage label="Settings">{places.map((p) => <NavTile key={p.id} {...p} />)}</SettingsPage>