Skip to content

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.

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.

Projects

Everything the team is shipping this quarter.

Page content
<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>

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.

Dashboard

<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>

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.

AT

Dashboard

Page content
<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>

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.

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>

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.

Acme

Dashboard

Menu
<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.

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.

Inbox

Message list
<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>

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

Main content here
<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>

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>

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

Active Admin · joined March 2024

Profile

Email
a.turing@example.com
Team
Platform
Last active
4 minutes ago
<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 &middot; 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>

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>

For the table itself with a toolbar, status badges and pagination, see the table assembly.

Users

Name Email 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>

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>