# Choose before you build

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from core/site/guides/content/choosing-what-to-build-with.md and the template, layout and pattern meta files. Do not edit: change the source and run the generator.

Before you write markup for a page: (1) name the page type or job, (2) look it up in the use-case table below, (3) open the template, layout, pattern or element it names, (4) copy it and change content, slots, props and tokens only. Prefer a template over a layout, a layout over a pattern, a pattern over your own composition, an element over a `<div>`. If nothing fits, say so and record the gap; do not invent a tag or a component.

Plainkit ships more than elements. It ships page templates, page layouts, composed patterns and, in Blazor, components for whole record pages. Most of the time an app that struggles with Plainkit is not missing a feature: it is hand-building something that already exists (a table, a modal, a header search, a record form) and then fighting the SDK's own CSS to make it fit. This guide is about choosing, before you write markup. The rule of thumb: the more of the page you take from the SDK, the less you maintain.

## The decision path

Go down this list and stop at the first step that fits.

1. **A template fits the page type.** A template is a full page (list and detail, dashboard, settings form, wizard, sign in, workspace). Paste it, then change the content. The catalogue is in the agent skill's templates reference and on the gallery's Templates page.
2. **A layout fits the page's anatomy.** A layout is the skeleton of a kind of page (list, record, record with sidebar, setup, tool, wizard) with placeholder content. Take it when no template matches but the page is one of those kinds (layouts reference).
3. **Compose patterns.** A pattern is a small composition for one job (confirm before delete, filter bar and table, forms, notifications, onboarding, search results, settings with unsaved changes). Put one or two inside your own page (patterns reference).
4. **Use the element that names the job.** A table is `pk-table`, a modal is `pk-dialog`, tabs are `pk-tabs`, a header search is `pk-app-bar-search`. Look the job up in the elements index before writing a `<div>`.
5. **Extend, never override.** Change what an element or template shows through its slots, its props, its `::part()` names, its `--pk-*` custom properties and the tokens. Do not reach into its shadow tree, copy its markup or fight its CSS.
6. **Write your own only for what the SDK does not cover.** Then keep it small, build it from `pk-stack`, `pk-cluster`, `pk-grid` and `pk-text`, use tokens for every colour and size, and record the gap (see "Ask for a missing component").

## Use cases: where to start

Only things that exist today are listed. A dash means there is no ready-made piece: compose from the elements named. The "In an app (SDK)" column is for an app mounted with `mountApp`: there the page is not markup you paste but a page type you give a config, and Build an app walks through it.

| Page type or job | Start from | In an app (SDK) | In Blazor |
|---|---|---|---|
| Whole app frame: side nav, header, body | Template `app` (a runnable app); template `overlays-nav` for the composition; element `pk-app-shell` with `pk-side-nav` or `pk-navbar` | `mountApp` with modules (see Build an app) | `PkAppShell`, `PkSideNav` |
| A plain content page: breadcrumbs, title, actions, content, footer | Template `page`; element `pk-page-header` | `mountPage` with `custom`, or `doc` for long-form text | `PkPageHeader`, or `PageBase` for its state |
| Static informational page: a heading and a block of prose, no data | Template `page`; elements `pk-heading`, `pk-card` | Page type `note` | `PkNotePage` |
| List page (search, table, rows open a record) | Template `routed-pair`; layout `list`; element `pk-data-table` | Page type `list` | `PkTable`, or `PkDataTable` when the server pages, searches and sorts |
| A list and its record | Template `routed-pair`: two routed pages, `/things` and `/things/{id}` (the default; see "Which list shape" below) | Page types `list` and `record`, the record route under the list | `PkDataTable` or `PkTable`; routes and `NavigationManager` for the record |
| Record create and edit | Layout `record`; layout `record-detail` (main body plus sticky sidebar); element `pk-record-form` (toolbar, tabs, error, sidebar around your form) or `pk-detail-layout` | Page type `record` | `PkRecordForm` with `PkFieldGroup` and `PkRecordEditor` |
| Read-only record or key figures | Layout `record-detail`; elements `pk-field-list`, `pk-stat` | Page type `record` with `editable: false` | `PkFieldList`, `PkStat` |
| Settings form | Template `form`; pattern `unsaved-settings` for a sticky save bar; elements `pk-form`, `pk-form-section` | Page type `settings` (a section nav beside the cards from three sections, `nav` to force or disable it) | `PkForm` with `PkFormSection`; `PkSettingsPage` for the page type |
| Long form built from a list of fields | Pattern `forms` for the controls, or `pk-field-group` to draw them from a list of field specs | Page type `record` | `PkFieldGroup` with `PkFieldSpec` |
| Print a document, edit a design, show tokens | `pk-print-page`, `pk-canvas`, `pk-design-surface`, `pk-specimen`; `pk-page-shell` for a headed page | Page type `print`; else `custom` | `PkPrintPage`, `PkCanvas`, `PkDesignSurface`, `PkSpecimen`, `PkPageShell` |
| Dashboard or report | Template `dashboard`; pattern `data-display`; elements `pk-stat`, `pk-chart`; for tabbed, independently loading widgets with filters `pk-dashboard-page` | Page type `dashboard` | `PkStat`, `PkChart`, `PkDashboardPage` |
| At-a-glance overview: area is a count, colour a status ("issues by area") | `pk-heat-map` (package `plainkit.canvas`); never hand-computed `pk-grid` spans | Page type `dashboard` | `PkHeatMap` (`Items` of `PkHeatItem`) |
| Search | Pattern `search-results`; element `pk-app-bar-search` in the shell header; `pk-command-palette` for a keyboard launcher | `search` in the app config, or `search(query, ctx)` on a module | `PkAppBarSearch`, `PkCommandPalette` |
| Filtered table | Pattern `filter-table` (a `pk-data-table`); element `pk-table-filters` | Page type `list` (its `filters`) | `PkTableFilters`, `PkDataTable` |
| Wizard or guided flow | Template `wizard`; layout `wizard`; elements `pk-stepper`, `pk-step` | Page type `wizard` | `PkStepper` |
| Tool page (one input, one outcome) | Layout `tool`; template `workspace` for a multi-pane tool | Page type `tool`; `workspace` for a multi-pane tool | `PkWorkspace` |
| Resizable side-by-side panels (Toolbox, Canvas, Properties; file tree, editor, log) | Element `pk-dock`: each panel is a child with `slot="<id>"`, `data-heading` and `data-group` (left, center, right, bottom; panels sharing a group become tabs); the arrangement is a JSON `layout` and a layout-change event reports each resize or tab change; `persist-key` saves and restores the layout across reloads; moving, floating and collapse are not built yet (issue 432) | None: compose the panels yourself | `PkDock` |
| Sign in | Template `auth` | None: a plain page before the app mounts | `PkCard` with `PkField` and `PkInput` |
| Empty, error and loading regions | Template `states`; elements `pk-empty-state`, `pk-skeleton`, `pk-spinner`, `pk-alert` | Page types `states` and `not-found` (the app draws its own loading and error boundaries) | `PkEmptyState`, `PkSkeleton` |
| Confirm a destructive action | Pattern `confirm-delete`; element `pk-dialog` | `ctx.dialogs.confirm(config)` | `PkDialog` |
| Toasts and notices | Pattern `notifications`; elements `pk-toast-stack`, `pk-alert` | `ctx.notify.success(title)` | `PkToastStack` |
| Onboarding checklist | Pattern `onboarding` | Page type `wizard` | `PkStepper`, `PkProgress` |
| Short list of small records with an add form | Layout `setup` | Page type `list` with a `record` route under it | `PkTable` with `PkField` |
| Marketing or landing page | Template `landing` (hero, feature grid, pricing, FAQ); pattern `pricing-cards`; `pk-quote`, `pk-cta-band`, pattern `marketing-sections` (guide "Marketing sections"); composed from `pk-container`, `pk-stack`, `pk-grid`, `pk-card`, `pk-text` and `pk-button`, tokens only, no hero background images (imagery goes in `pk-media`). | None | same |
| Rich text input | Pattern `markdown-editor`: `pk-textarea` holding Markdown plus a `pk-tabs` Write and Preview. | None | `PkTextarea` with `PkTabs` |
| Panes: `pk-workspace` or `pk-dock` | `pk-workspace` for a content page with a fixed nav, main and aside; `pk-dock` for a tool whose user rearranges, resizes or tabs panels and expects the layout to persist. Keep both until dock's floating and rail steps land (issues 618, 608); then compare dock footprint (a fixed preset over about 2x workspace gzip means keep both), shared phone-strip code, real consumers and Blazor wrappers | Page type `tool`; `workspace` for a multi-pane tool | `PkWorkspace`, `PkDock` |

## A heat map: area for a count, colour for a status

`pk-heat-map` fills a fixed-height area (`--pk-heat-map-height`, default 20rem) with boxes whose area is each item's `value` and whose colour is its `severity` (neutral, info, success, warning, danger) or, with `scale="intensity"`, one hue by `intensity` 0 to 1. Boxes are real links (with `href`) or buttons, the whole map is one tab stop with arrow-key movement, and a data table is always kept for assistive technology (`show-table` shows it instead of the map).

Give it data in one of two ways. The `items` property is `[{ id, label, value, severity?, intensity?, href?, children? }]`. Or slot a plain `<table>` as the direct child: the first cell of a row is the name (a link in it is the `href`), the value is column 2 unless a header says otherwise, a header containing "severity" or "status" and one containing "intensity" pick those columns, a cell's `data-value` beats its text (so "1,204" or "$1.2k" can carry a number) and a row's `data-id` is the id. A row with no number above 0 is skipped and logged once. If `items` is set it wins over the table; edit the table and the map redraws.

Useful options: `layout="rows"` keeps the input order so positions stay put while live values change, `group-by` draws items with `children` as framed areas (one level), `max-items` and `min-box` merge the small ones into one "Other (n)" box (it takes the worst severity of its members and opens its members as a table), and `value-format` is number, compact or share. Events: a select event (cancel it to stop a link navigating) and a focus-item event, both with `detail.id`. In Blazor pass `Items` (`PkHeatItem` records, `Children` for groups) and use `OnSelect` and `OnFocusItem`; the slotted table is for markup-first pages, Blazor uses `Items`.

## Which list shape

A list and its record are two routed pages. A single page that shows a list and its record together (the record stacked below the list or beside it) is not offered as an equal option: it scrolls three ways at once (the page, the list, the record), the record is off screen whenever the list is in view, and a row click gives no sense of having gone anywhere. It is the easiest shape to build, so it is the one that gets built by default. Do not build it.

| You need | Use |
|---|---|
| Rows you draw yourself, no paging or search | `pk-table` |
| One dataset with search, sort, paging and selection inside a page of your own layout | `pk-data-table` (pattern `filter-table`) |
| A form field that picks one record from a long list (customers, products, users) in a popup table | `pk-lookup-picker` (the list is too long for `pk-combobox`) |
| A form field for formatted text stored as Markdown (bold, italic, lists, links, a preview) | `pk-markdown-editor` (show stored text read-only with `pk-markdown`, never by writing the text into markup) |
| A whole route that lists a collection (title bar, filters, row links) | Page type `list` |
| A list and its record | `list` and `record` as two routes (template `routed-pair`). The list fills the viewport and is the only thing that scrolls |
| A peek at context that is not a record of its own | A flyout over the list (`pk-drawer`) |
| A tool shell with panes (list in the main pane, record in the aside) | Template `routed-list-detail` ("Workspace list and record"), the workspace variant: not the default |

## Customize a template

A template is a starting point you own once you paste it. Change these freely:

- **Content**: text, data, the rows and columns, which fields exist, the labels.
- **Which elements are present**: delete a card, add a `pk-tab`, swap one `pk-stat` for another.
- **Slots and props**: put your own button in `slot="actions"`, set `heading`, `variant` or `size`, all of which the element's reference lists.
- **The page script**: keep the script the template shows and edit its data and handlers. The demo shell around a template (`mountChrome`) is not part of it.
- **Tokens**: recolour or resize by overriding a token in your own stylesheet (Theming and tokens).

Do not change these:

- **Do not copy an element's internals into the page.** If the markup you want is what the element's shadow tree already draws, use the element.
- **Do not add a wrapper element to fix spacing.** Use `pk-stack`, `pk-cluster` or `pk-grid` with a `gap`, or a token.
- **Do not override an element's CSS from outside** (a selector aimed at its internals, an `!important`). If a `--pk-*` custom property or a `::part()` does not exist for what you need, that is a gap to file, not a reason to override.
- **Do not turn the page's structure inside out.** The elements respond to width by themselves (Responsive design); a template that already collapses on a phone keeps doing so only while you keep its structure.

A template extended through its extension points looks like this:

```html
<pk-page-header heading="Orders" level="1">
  <pk-button slot="actions" variant="primary">New order</pk-button>
</pk-page-header>
```

and recoloured through a token, never a literal colour:

```css
:root[data-theme="dark"] {
    --color-accent: #7c3aed;
}
```

## Anti-patterns

| Instead of | Use | Why |
|---|---|---|
| Your own table markup with sort, select and paging | `pk-table` (Blazor: `PkTable` or `PkDataTable`) | Keyboard, sorting, selection, row menus, an empty state and the phone layout are already done and tested |
| A `<div>` overlay with your own focus handling for a modal | `pk-dialog`, opened with `data-open` or `PkDialog.confirm` | Focus trap, Escape, backdrop and scroll lock come with it |
| Tabs from buttons and `hidden` panels | `pk-tabs` with `pk-tab` and `pk-tab-panel` | Arrow keys, roles and the phone strip |
| A hand-written `PkField` plus `PkInput` wrapper component per field | `PkFieldGroup` with a list of `PkFieldSpec` | One place for label, hint, help, validation, options, conditional fields and read-only state |
| A header search box built from an `<input>` | `pk-app-bar-search` in the `pk-app-shell` header | It collapses to an icon on a phone, shows grouped results and coordinates with the shell |
| A repeated create-or-edit page (toolbar, error, save state) copied per record | `PkRecordForm` with `PkRecordEditor` | Load, validate, save, delete, busy and error are one implementation |
| Copying the same title, error and busy bookkeeping into every page | `PageBase` (Blazor) or `createPage` (SDK) | Status, counted busy with a framework-owned loading overlay (no overlay markup), breadcrumbs and logging as configuration |
| A `display: contents` wrapper or a wrapper `<div>` between a shell, nav or dropdown and its slotted children | Put the slotted content directly in the slot; use the component's own parameters | A wrapper breaks the parent's direct-child lookups and `::slotted` rules |
| `!important` or a selector into an element's internals | A slot, a prop, a `::part()`, a `--pk-*` property or a token | An override breaks at the next release and is invisible to the scorecard |
| A literal colour, pixel size or duration | A token (`--color-*`, `--space-*`, `--text-*`, `--radius-*`, `--duration-*`) | Themes, density and dark mode only reach tokens |
| The removed `u-*` utility classes and the retired class-based components | `pk-text`, `pk-stack`, `pk-cluster`, `pk-grid` and the elements (Migrating from the compat classes) | Removed classes fail silently: the styling just disappears |
| A `<p>` or `<span>` with a class for text | `pk-text` | Tone, variant and font are props, not classes |
| Your own toast, tooltip or dropdown | `pk-toast-stack`, `pk-tooltip`, `pk-dropdown` | Position, focus and dismissal are done |

## When you must write your own

Write your own only when the SDK has nothing for the job and composing existing elements cannot do it. Then:

- Compose from `pk-stack`, `pk-cluster`, `pk-grid`, `pk-text` and `pk-card`; do not invent a `pk-*` tag.
- Style with tokens in a stylesheet (no inline `style`, no `<style>` element), so it works under the strict content security policy.
- Keep it in one place, named for the job, so it can be replaced when the SDK adds the element.
- Record the gap (next section).

## In Blazor

- **Every page starts as a `PageBase`** when it needs a title, breadcrumbs, a status notice or a busy overlay: derive from it instead of repeating that state.
- **A record page is three pieces**: `PkRecordForm` draws it (toolbar, tabs, error, cards, sidebar), `PkFieldGroup` renders the fields from `PkFieldSpec` items, `PkRecordEditor` holds the load, validate, save and delete state.
- **A list is `PkTable`** for rows you already have, and **`PkDataTable`** when the list is searched, sorted and paged on the server.
- **The frame is `PkAppShell`**, with `PkSideNav` (it follows the route by itself) and `PkAppBarSearch` in the header.
- **An element with no component** is used as raw markup with `@onpk-...` handlers, not re-wrapped by hand.

```razor
<PkRecordForm OnValid="SaveAsync" Busy="_busy" Error="@_error">
    <form @onsubmit:preventDefault>
        <PkStack>
            <PkCard Heading="Details">
                <PkFieldGroup TItem="Location" Fields="_fields" Model="_location" />
            </PkCard>
        </PkStack>
    </form>
</PkRecordForm>

@code {
    private Location _location = new();
    private bool _busy;
    private string? _error;
    private IReadOnlyList<PkFieldSpec<Location>> _fields =
    [
        new() { Key = "name", Label = "Name", Required = true, Get = l => l.Name, Set = (l, v) => l.Name = v ?? "" },
    ];
    private Task SaveAsync() => Task.CompletedTask;
}
```

## Ask for a missing component

If the decision path ends at "write your own", the SDK has a gap, and the gap is worth recording so the next app does not rebuild it.

1. Search the open issues in the repository for the job first (a component may be planned).
2. If there is none, add it to the standing issue "Tracker: components the SDK lacks" (search the repository's issues for that title) or open an issue that describes the job, the page it appears on, what you composed from existing elements in the meantime and what was awkward about it.
3. Build the interim version from existing elements only, in one place, named for the job.
4. Do not fix the gap by patching the SDK's own CSS in your app.

## Catalogue: templates (full pages, templates.md)

- `page`: Breadcrumbs, title with optional back button, page actions, content, footer.
- `app`: A whole app from one config: mountApp builds the chrome and each module supplies routes of built-in page types: an overview (dashboard) and an inbox (master-detail).
- `workspace`: A nav rail, one tab strip over a content pane and a docked inspector. Fills the viewport; rail and pane scroll on their own.
- `routed-pair`: The canonical list-and-record shape: a list page (search, filter, sort, paging) and a record page (form, save, dirty guard) as two separately routed pages. A row click navigates to the record; Save navigates back.
- `dashboard`: Stat tiles, a chart area and a table.
- `form`: Sectioned form with help and validation states and a sticky save bar.
- `wizard`: A stepper, one card per step, Back and Next.
- `states`: What a region shows with no data, on failure and while loading.
- `auth`: A centred card on a centred shell.
- `landing`: A marketing page composed from existing elements: hero, feature grid, pricing cards and FAQ. Colours and sizes come from tokens; imagery, if any, goes in pk-media.
- `overlays-nav`: An app page built from the overlay, feedback and navigation elements: app shell with a filterable side nav, breadcrumb, banner, stepper, on-this-page list, dropdown, select, tooltip, confirm popover, drawer, dialog, context menu, loading overlay, pager, command palette and back to top.
- `routed-list-detail`: The workspace (tool shell) variant, not the default list-and-record shape (see "List page and record page"): a pk-data-table (search, status filter, sort, pager) in the main pane and the open record in the aside, driven by the route.

## Catalogue: layouts (page anatomies, layouts.md)

- `list`: A search page: crumbs and actions, a toolbar and a grid whose rows navigate to a record page.
- `record`: One record: chips and actions under the top bar, a tab strip and read-only fields beside editable ones.
- `setup`: A short list of small records with an add form above it.
- `tool`: A one-shot operation: explain it, take an input, show the outcome.
- `wizard`: A guided create flow: numbered steps, one card at a time, Back and Next.
- `record-detail`: One record's body plus a sticky sidebar of summary cards, collapsing to one column below the container's own width, not the viewport.

## Catalogue: patterns (small compositions, patterns.md)

- `filter-table`: A pk-data-table: search, a status filter, sortable columns, a pager and row selection from one load(query) callback; choosing rows reveals a bulk-action bar.
- `confirm-delete`: A destructive action asks first: the modal names what will be lost and offers Cancel; the backdrop never dismisses it.
- `unsaved-settings`: A sectioned form with a sticky save bar that says there are unsaved changes and offers Discard.
- `notifications`: Transient toasts for confirmations, inline notices for what needs attention, and a count badge on the bell.
- `search-results`: A search field with a keyboard hint over results grouped by type, each row showing a snippet with the match marked.
- `onboarding`: A short checklist with a progress bar: done, current and upcoming steps, and one clear next action.
- `data-display`: A report page: key figures with change and trend, charts drawn from tables, a sortable selectable table with paging, activity, people, reference fields, a calendar, a tree and code, all as elements.
- `forms`: A settings form built only from form elements: sectioned fields with help, counters and validation, money and stepper inputs, dual range, choice controls, tag and colour inputs, a drop zone and a sticky action bar.
- `pricing-cards`: Plans side by side: a name, a price, three or four inclusions and one action per card, with a badge on the recommended plan.
- `markdown-editor`: The supported answer to rich text: a pk-textarea holding Markdown, a Preview tab that draws it, no contenteditable and no HTML stored.
- `marketing-sections`: The four blocks of a marketing page: a testimonial, a logo strip, a contact block and a closing call-to-action band.
