Blueprints
Whole-page compositions. Each blueprint below is the structure of an entire screen — chrome, regions and content areas — built from Airframe patterns and plain HTML. Copy, paste, customise.
Looking for a single section instead? Heroes, pricing tables, sign-in cards, data tables and footers live in Assemblies. The layout primitives these blueprints are built from are documented under Layouts.
The first group is app chrome — the frame that stays put as you navigate. The second is page compositions — what fills the content area.
Stacked Shell
Section titled “Stacked Shell”Top navigation, a page header band, then full-width content. The simplest shell and the one most apps start with.
af-navbar already draws the bottom border; the header band picks up its tint from af-bg-surface-secondary. Drop either for a flush header. af-sticky af-top-0 pins the nav while the page scrolls.
<div class="af-stack af-gap-0">
<nav class="af-navbar af-sticky af-top-0" aria-label="Main">
<div class="af-container af-spread">
<a class="af-navbar-brand" href="/">Acme</a>
<ul class="af-navbar-nav">
<li>
<a href="#" aria-current="page">Dashboard</a>
</li>
<li>
<a href="#">Projects</a>
</li>
<li>
<a href="#">Team</a>
</li>
</ul>
</div>
</nav>
<div class="af-bg-surface-secondary af-py-lg">
<div class="af-container af-stack af-gap-xs">
<h1>Projects</h1>
<p class="af-text-muted">Everything the team is shipping this quarter.</p>
</div>
</div>
<main class="af-container af-py-xl">
<div class="af-card">
<div class="af-card__body">Page content</div>
</div>
</main>
</div>App Shell
Section titled “App Shell”Full-viewport dashboard chrome. See App shell. Width is --af-sidebar-width. Distinct from af-sidebar (in-page content + aside grid) and from the two-column split further down.
<div class="af-app">
<aside class="af-app__sidebar">
<nav aria-label="App">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Settings</a>
</li>
</ul>
</nav>
</aside>
<main class="af-app__main af-p-xl">
<h1>Dashboard</h1>
</main>
</div>App Shell With Top Bar
Section titled “App Shell With Top Bar”Sidebar plus a bar across the content area for search, account and page-level actions.
af-app has no header part on purpose: from lg it is a flex row, and every direct <aside> or <nav> child is sized as the sidebar column. A bar placed as a direct child of af-app would therefore become a second column. Put it inside af-app__main, where af-sticky af-top-0 pins it as the content scrolls.
<div class="af-app">
<aside class="af-app__sidebar af-p-lg">
<nav aria-label="App">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Reports</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Settings</a>
</li>
</ul>
</nav>
</aside>
<main class="af-app__main">
<div class="af-navbar af-sticky af-top-0">
<form role="search">
<input class="af-input" type="search" placeholder="Search" aria-label="Search">
</form>
<div class="af-inline-sm">
<button class="af-btn af-is-secondary af-is-sm">Invite</button>
<span class="af-avatar">AT</span>
</div>
</div>
<div class="af-p-xl af-stack af-gap-lg">
<h1>Dashboard</h1>
<div class="af-card">
<div class="af-card__body">Page content</div>
</div>
</div>
</main>
</div>Dark Sidebar
Section titled “Dark Sidebar”The same shell with an inverted rail. There is no sidebar-specific CSS: data-theme="dark" re-resolves the colour tokens for that subtree, so af-side-nav links, borders and muted text all follow, and af-bg-surface paints the panel with the dark surface.
<div class="af-app">
<aside class="af-app__sidebar af-bg-surface af-p-lg" data-theme="dark">
<nav aria-label="App">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Reports</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Settings</a>
</li>
</ul>
</nav>
</aside>
<main class="af-app__main af-p-xl">
<h1>Dashboard</h1>
</main>
</div>For a brand rail instead of a dark one, point the two tokens the sidebar consumes at your brand colour: style="--af-color-surface-primary: var(--af-color-primary); --af-color-text: var(--af-color-on-primary)". See Theming.
Icon Rail
Section titled “Icon Rail”A narrow icon-only rail is the same shell with a smaller --af-sidebar-width. Each link keeps a real text label for screen readers via af-sr-only, and the icons are marked aria-hidden.
<div class="af-app" style="--af-sidebar-width: 4.5rem">
<aside class="af-app__sidebar af-py-md">
<nav aria-label="App">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link af-text-center" href="#" aria-current="page">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
</svg>
<span class="af-sr-only">Dashboard</span>
</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link af-text-center" href="#">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 20V10M10 20V4M16 20v-6M2 20h20" />
</svg>
<span class="af-sr-only">Reports</span>
</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link af-text-center" href="#">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
<path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
</svg>
<span class="af-sr-only">Settings</span>
</a>
</li>
</ul>
</nav>
</aside>
<main class="af-app__main af-p-xl">
<h1>Dashboard</h1>
</main>
</div>Mobile Navigation Drawer
Section titled “Mobile Navigation Drawer”The responsive half of every sidebar shell: an in-flow rail from lg up, and an overlay Drawer below it. The drawer is a native <dialog>, so showModal() handles the backdrop, ESC and scroll lock — no library.
af-hidden af-show@lg hides the rail and brings it back at lg; af-hidden@lg does the reverse for the menu bar. Pair af-hidden with af-show@{bp} specifically — af-hidden is display: none !important at every width, and a display utility such as af-block@lg will not override it. See Responsive Suffix.
<div class="af-app">
<aside class="af-app__sidebar af-hidden af-show@lg af-p-lg">
<nav aria-label="App">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Reports</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Settings</a>
</li>
</ul>
</nav>
</aside>
<main class="af-app__main">
<div class="af-navbar af-hidden@lg">
<button class="af-btn af-btn-icon af-is-outline af-is-secondary" aria-label="Open navigation"
onclick="document.getElementById('app-nav').showModal()">☰</button>
<span class="af-navbar-brand">Acme</span>
</div>
<div class="af-p-xl">
<h1>Dashboard</h1>
</div>
</main>
</div>
<dialog class="af-drawer af-has-slide" id="app-nav"
onclick="if (event.target === this) this.close()">
<div class="af-drawer-header">
<span class="af-drawer-title">Menu</span>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</div>
<div class="af-drawer-body">
<nav aria-label="App (mobile)">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Reports</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Settings</a>
</li>
</ul>
</nav>
</div>
</dialog>The preview keeps the menu bar visible at every width so the drawer is clickable on a desktop; the code drops it from lg up with af-hidden@lg. Keep the two navs in sync, and give each its own aria-label.
Three-Column Layout
Section titled “Three-Column Layout”Inbox and detail screens: primary nav on the left, a content column, and a secondary column on the right. af-app supplies the outer chrome and af-sidebar splits the content area into a 1fr column plus an aside, which appears from md and stacks underneath on smaller screens.
af-sticky af-top-0 keeps the aside in view while the content column scrolls. For a constrained variant, wrap the split in af-container-lg.
<div class="af-app">
<aside class="af-app__sidebar af-p-lg">
<nav aria-label="App">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Inbox</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Archive</a>
</li>
</ul>
</nav>
</aside>
<main class="af-app__main af-p-xl">
<div class="af-sidebar">
<div class="af-stack af-gap-lg">
<h1>Inbox</h1>
<div class="af-card">
<div class="af-card__body">Message list</div>
</div>
</div>
<aside class="af-sticky af-top-0">
<div class="af-card">
<h3 class="af-card__title">Details</h3>
<div class="af-card__body">
<dl class="af-dl">
<dt>Status</dt>
<dd>Open</dd>
<dt>Assignee</dt>
<dd>A. Turing</dd>
</dl>
</div>
</div>
</aside>
</div>
</main>
</div>Two-Column Split
Section titled “Two-Column Split”An in-page 12-column split — section nav beside content, inside a page that already has its own chrome. The grid auto-detects 12 columns when children carry af-col-span-*. For full-app chrome prefer af-app; for an overlay nav on small screens use the drawer shell above.
A column carrying only a breakpoint span is full width below that breakpoint, so the two stack on small screens without any extra classes. Add a base af-col-span-* only when you want a different split before lg.
Do not put af-drawer on an in-flow column. It is a fixed-position overlay that stays display: none until its <dialog> is opened, so the column would disappear.
Dashboard
<div class="af-grid af-gap-0">
<aside class="af-col-span-3@lg af-surface af-p-lg">
<nav aria-label="Section">
<ul class="af-side-nav">
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#" aria-current="page">Dashboard</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Settings</a>
</li>
<li class="af-side-nav-item">
<a class="af-side-nav-link" href="#">Profile</a>
</li>
</ul>
</nav>
</aside>
<main class="af-col-span-9@lg af-p-xl">
<div class="af-stack af-gap-lg">
<h1>Dashboard</h1>
<div class="af-card">
<div class="af-card__body">Main content here</div>
</div>
</div>
</main>
</div>Dashboard Layout
Section titled “Dashboard Layout”Note: This uses af-grid with column counts for equal-width cards. Prefer this over af-cols-12 for card grids.
Dashboard
Users
1,234
+12% from last month
Revenue
$12,345
+8% from last month
Orders
567
+5% from last month
Growth
+23%
Overall growth rate
<div class="af-container">
<div class="af-stack af-gap-lg">
<header class="af-inline af-justify-between">
<h1>Dashboard</h1>
<div class="af-inline af-gap-sm">
<button class="af-btn af-is-secondary">Export</button>
<button class="af-btn">New</button>
</div>
</header>
<div class="af-grid-1 af-grid-2@md af-grid-4@lg af-gap-lg">
<div class="af-card">
<h3 class="af-card__title">Users</h3>
<div class="af-card__body">
<p class="af-text-h2">1,234</p>
<p class="af-text-sm">+12% from last month</p>
</div>
</div>
<div class="af-card">
<h3 class="af-card__title">Revenue</h3>
<div class="af-card__body">
<p class="af-text-h2">$12,345</p>
<p class="af-text-sm">+8% from last month</p>
</div>
</div>
<div class="af-card">
<h3 class="af-card__title">Orders</h3>
<div class="af-card__body">
<p class="af-text-h2">567</p>
<p class="af-text-sm">+5% from last month</p>
</div>
</div>
<div class="af-card">
<h3 class="af-card__title">Growth</h3>
<div class="af-card__body">
<p class="af-text-h2">+23%</p>
<p class="af-text-sm">Overall growth rate</p>
</div>
</div>
</div>
</div>
</div>Detail Screen
Section titled “Detail Screen”One record: breadcrumbs and title, its fields in the content column, metadata and actions in the aside. The heading block is the page heading assembly; fields use af-dl.
A. Turing
<div class="af-container af-py-xl">
<div class="af-stack af-gap-lg">
<nav aria-label="Breadcrumb">
<ol class="af-breadcrumbs">
<li class="af-breadcrumbs-item">
<a href="/users" class="af-breadcrumbs-link">Users</a>
</li>
<li class="af-breadcrumbs-item" aria-current="page">A. Turing</li>
</ol>
</nav>
<div class="af-inline af-justify-between af-items-start af-wrap af-gap-md">
<div class="af-stack af-gap-xs">
<h1>A. Turing</h1>
<div class="af-inline af-gap-sm af-wrap">
<span class="af-badge af-is-success">Active</span>
<span class="af-text-sm af-text-muted">Admin · joined March 2024</span>
</div>
</div>
<div class="af-inline-sm">
<button class="af-btn af-is-secondary">Edit</button>
<button class="af-btn af-is-danger af-is-outline">Suspend</button>
</div>
</div>
<div class="af-sidebar">
<div class="af-card">
<h2 class="af-card__title">Profile</h2>
<div class="af-card__body">
<dl class="af-dl">
<dt>Email</dt>
<dd>a.turing@example.com</dd>
<dt>Team</dt>
<dd>Platform</dd>
<dt>Last active</dt>
<dd>4 minutes ago</dd>
</dl>
</div>
</div>
<aside class="af-stack af-gap-md">
<div class="af-card">
<h2 class="af-card__title">Access</h2>
<div class="af-card__body af-stack af-gap-sm">
<span class="af-badge">SSO</span>
<span class="af-text-sm af-text-muted">Two-factor authentication enabled</span>
</div>
</div>
</aside>
</div>
</div>
</div>Settings Page
Section titled “Settings Page”Section nav uses af-nav-pills (filled, scrolls horizontally on small screens). Action rows use af-action-row — not af-field-inline.
Notifications
Email alerts
When someone mentions you
<div class="af-container">
<div class="af-grid af-gap-xl">
<aside class="af-col-span-3@lg">
<nav class="af-nav-pills" aria-label="Settings">
<a href="#profile" aria-current="page">Profile</a>
<a href="#security">Security</a>
<a href="#notifications">Notifications</a>
</nav>
</aside>
<main class="af-col-span-9@lg">
<div class="af-stack af-gap-lg">
<h1>Notifications</h1>
<div class="af-action-row-group">
<div class="af-action-row">
<div class="af-action-row__copy">
<h2 class="af-action-row__title">Email alerts</h2>
<p class="af-action-row__hint">When someone mentions you</p>
</div>
<div class="af-action-row__control">
<input type="checkbox" role="switch" aria-label="Email alerts" checked>
</div>
</div>
</div>
</div>
</main>
</div>
</div>Table Page
Section titled “Table Page”For the table itself with a toolbar, status badges and pagination, see the table assembly.
Users
| Name | Role | Actions | |
|---|---|---|---|
| John Doe | john@example.com | Admin |
|
<div class="af-container">
<div class="af-stack af-gap-lg">
<div class="af-inline af-justify-between">
<h1>Users</h1>
<button class="af-btn">Add User</button>
</div>
<div class="af-card">
<div class="af-card__body">
<table class="af-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Role</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>John Doe</td>
<td>john@example.com</td>
<td>Admin</td>
<td>
<div class="af-inline af-gap-sm">
<button class="af-btn af-is-sm">Edit</button>
<button class="af-btn af-is-sm af-is-danger">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>In React
Section titled “In React”Every blueprint works identically in React — use className instead of class. See React recipes for the framework-specific notes.
<div className="af-stack af-gap-lg">
<h1>Dashboard</h1>
<div className="af-card">
<div className="af-card__body">Content</div>
</div>
</div>