Composition Recipes
Use this guide when building complete app screens from Porchlight primitives. It is intentionally prescriptive: pick a recipe, keep the semantic component contracts intact, and add only small app-specific glue where the layout needs it.
Model checklist
- Use
.l-*layout primitives for page structure:.l-app-shell,.l-container,.l-stack,.l-grid,.l-cluster,.l-sidebar. - Use
.c-*components for UI semantics and component chrome: cards, forms, tables, toolbars, tabs, pagination, badges, nav, stats. - Use
.u-*utilities for small text and overflow adjustments:.u-muted,.u-muted-sm,.u-truncate,.u-sr-only,.u-min-0. - Use app CSS only for glue: page-specific widths, a one-off split, or a local
alignment fix. Prefer tokens such as
--pl-space-4over new spacing values. - Keep native HTML semantics: real tables, forms, headings, labels, nav, buttons, links, fieldsets, and legends.
Avoid these mistakes
- Do not make div-based tables. Use
<table>inside.c-table-wrap. - Do not put page headers inside
.c-toolbar; use.c-page-headerinside padded content and.c-toolbaron data-region edges. - Do not nest cards inside cards for layout. Use
.l-grid,.l-sidebar,.l-stack, or sibling.c-cardelements instead. - Do not omit
.c-table-wrap; it provides scroll, borders, sticky context, and container-query behavior for.c-table. - Do not invent spacing systems. Use
.l-stack,.l-grid,.l-cluster, and--pl-space-*tokens. - Do not use visible labels as placeholders only. Use real visible labels or
.u-sr-onlyfor compact controls.
App Shell Recipe
Use this for SaaS workspaces, dashboards, inboxes, consoles, and admin tools.
The shell owns the topbar/sidebar/main regions; the page content goes inside
.l-app-shell__main.
<div class="l-app-shell">
<header class="l-app-shell__topbar">
<div class="l-cluster" style="--l-cluster-justify: space-between;">
<strong>Acme</strong>
<button class="c-button" data-variant="ghost">Search</button>
</div>
</header>
<aside class="l-app-shell__sidebar">
<nav class="c-nav" aria-label="Main navigation">
<a class="c-nav__item" href="/dashboard" aria-current="page">
<span class="c-nav__label">Dashboard</span>
</a>
<a class="c-nav__item" href="/accounts">
<span class="c-nav__label">Accounts</span>
</a>
</nav>
</aside>
<main class="l-app-shell__main">
<div class="l-container l-stack" style="--l-stack-gap: var(--pl-space-6);">
<!-- page sections -->
</div>
</main>
</div>
Dashboard Recipe
Use .c-page-header for the page title and actions, .l-grid for responsive
metric cards, and .c-card[data-surface="app"] around each KPI.
<div class="l-container l-stack" style="--l-stack-gap: var(--pl-space-6);">
<div class="c-page-header">
<div class="c-page-header__heading">
<h1 class="c-page-header__title">Dashboard</h1>
<span class="c-page-header__subtitle">Overview of workspace health</span>
</div>
<div class="c-page-header__actions">
<button class="c-button" data-variant="secondary">Export</button>
<button class="c-button" data-variant="primary">New report</button>
</div>
</div>
<div class="l-grid" style="--l-grid-min: 14rem;">
<section class="c-card" data-surface="app">
<div class="c-card__body">
<div class="c-stat">
<span class="c-stat__label">Monthly revenue</span>
<div class="c-stat__value">
$48,200<span class="c-stat__unit">/mo</span>
</div>
<span class="c-stat__trend" data-direction="up">12.4%</span>
</div>
</div>
</section>
</div>
</div>
Data Region Recipe
Use this for tables with headers, filters, actions, selected rows, and
pagination. The card frames the region; toolbars sit at the top and bottom;
the table must stay inside .c-table-wrap.
<section class="c-card" data-surface="app">
<div class="c-toolbar">
<div class="c-toolbar__group">
<label class="c-field">
<span class="u-sr-only">Search accounts</span>
<input
class="c-field__control"
type="search"
placeholder="Search accounts..."
/>
</label>
<button class="c-button" data-variant="ghost">Filter</button>
</div>
<div class="c-toolbar__group">
<button class="c-button" data-variant="secondary">Export</button>
<button class="c-button" data-variant="primary">New account</button>
</div>
</div>
<div class="c-table-wrap" style="border-inline: 0; border-radius: 0;">
<table class="c-table" style="--c-table-min: 42rem;">
<thead>
<tr>
<th class="c-table__check">
<input type="checkbox" aria-label="Select all" />
</th>
<th class="c-table__sticky-col" data-sort="asc">
Account <span class="c-table__sort-icon"></span>
</th>
<th>Status</th>
<th data-align="end">Seats</th>
<th data-align="end" data-sort="desc">
MRR <span class="c-table__sort-icon"></span>
</th>
</tr>
</thead>
<tbody>
<tr aria-selected="true">
<td class="c-table__check">
<input type="checkbox" checked aria-label="Select Acme" />
</td>
<td class="c-table__sticky-col">Acme Ops</td>
<td><span class="c-badge" data-tone="success">Active</span></td>
<td data-align="end">48</td>
<td data-align="end">$2,400</td>
</tr>
</tbody>
</table>
</div>
<div
class="c-toolbar"
style="border-block-end: 0; border-block-start: 1px solid var(--pl-color-border);"
>
<div class="c-toolbar__group">
<span class="u-muted-sm">Showing 1-10 of 247</span>
</div>
<div class="c-toolbar__group">
<nav class="c-pagination" aria-label="Accounts pagination">
<button class="c-pagination__nav" disabled>Prev</button>
<button class="c-pagination__page" aria-current="page">1</button>
<button class="c-pagination__page">2</button>
<button class="c-pagination__nav">Next</button>
</nav>
</div>
</div>
</section>
Settings Page Recipe
Use .l-sidebar for local navigation plus a flexible settings panel. Use tabs
for major sections and .c-form for real form layout.
<div class="l-container">
<div class="l-sidebar" style="--l-sidebar-size: 14rem;">
<nav class="c-nav" aria-label="Settings sections">
<a class="c-nav__item" href="#profile" aria-current="page">
<span class="c-nav__label">Profile</span>
</a>
<a class="c-nav__item" href="#billing">
<span class="c-nav__label">Billing</span>
</a>
</nav>
<section class="c-card">
<div class="c-card__body">
<div class="c-tabs">
<div class="c-tabs__list" role="tablist" aria-label="Settings tabs">
<button class="c-tabs__tab" role="tab" aria-selected="true">
General
</button>
<button
class="c-tabs__tab"
role="tab"
aria-selected="false"
tabindex="-1"
>
Security
</button>
</div>
<div class="c-tabs__panel" role="tabpanel">
<form class="c-form">
<div class="c-form__grid">
<label class="c-field">
<span class="c-field__label">Workspace name</span>
<input class="c-field__control" value="Acme Ops" />
</label>
<div class="c-field">
<label class="c-field__label" for="workspace-slug"
>Workspace URL</label
>
<div class="c-input-group">
<input
id="workspace-slug"
class="c-field__control"
value="acme-ops"
/>
<span class="c-input-group__addon">.porchlight.app</span>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
</div>
</div>
Dense Admin View Recipe
Use data-density="dense" for screens where scan speed matters more than
breathing room. Pair it with compact copy, truncation, numeric alignment, and
real table semantics.
<main class="l-app-shell__main" data-density="dense">
<div class="l-container l-stack" style="--l-stack-gap: var(--pl-space-4);">
<div class="c-page-header">
<div class="c-page-header__heading">
<h1 class="c-page-header__title">Event queue</h1>
<span class="c-page-header__subtitle">1,248 events, 36 critical</span>
</div>
<div class="c-page-header__actions">
<button class="c-button" data-variant="secondary">Acknowledge</button>
</div>
</div>
<section class="c-card" data-surface="app">
<div class="c-toolbar">
<div class="c-toolbar__group">
<span class="u-muted-sm">Filtered to production</span>
</div>
<div class="c-toolbar__group">
<button class="c-button" data-variant="ghost">Refresh</button>
</div>
</div>
<div
class="c-table-wrap"
data-density="dense"
style="border-inline: 0; border-radius: 0;"
>
<table class="c-table" style="--c-table-min: 54rem;">
<thead>
<tr>
<th>Time</th>
<th>Host</th>
<th>Message</th>
<th data-align="end">Score</th>
</tr>
</thead>
<tbody>
<tr>
<td><time>10:42:18</time></td>
<td><code>api-04</code></td>
<td class="u-truncate" style="max-inline-size: 22rem;">
Unexpected auth spike from edge region
</td>
<td data-align="end">98</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>