Blocks

Real sections, ready to paste. Nothing to install first.

Copy, extend, reuse anywhere.

28 blocks · patterns · blueprints

Preview copy — plan names, prices, quotes and company names — is placeholder content for a fictional product, not an Airframe claim. Airframe is MIT licensed and free.

Marketing

Landing page sections — hero through closing call to action.

Announcement banner

Full-width bar for a launch, migration notice or maintenance window. af-banner carries the intent colour and its own bottom border, so it sits directly above the site header with nothing between them.

Built from Banner Link
Airframe 1.0 is out. Cascade layers, container queries and 60+ patterns. Read the changelog
<div class="af-banner af-is-primary">
  <strong>Airframe 1.0 is out.</strong>
  Cascade layers, container queries and 60+ patterns.
  <a href="/changelog" class="af-link">Read the changelog</a>
</div>

Marketing hero

Centered hero with a pill announcement, fluid headline, supporting lead, and a paired primary + outline call to action.

New v0.2.0 on npm

The structure your framework is missing.

One stylesheet, no runtime. Paste this section into React, Vue, Angular or plain HTML and it renders the same.

<section class="af-section-hero af-bg-surface-secondary">
  <div class="af-container af-container-md af-stack af-gap-xl af-items-center af-text-center">
    <a href="/changelog" class="af-pill">
      <span class="af-pill__tag">New</span>
      v0.2.0 on npm
      <span aria-hidden="true">&rarr;</span>
    </a>
    <hgroup class="af-stack af-gap-md">
      <h1 class="af-text-fluid-h1">The structure your framework is missing.</h1>
      <p class="af-lead af-text-muted af-measure">One stylesheet, no runtime. Paste this section into React, Vue, Angular or plain HTML and it renders the same.</p>
    </hgroup>
    <div class="af-inline af-justify-center af-wrap">
      <a href="/docs/installation" class="af-btn af-is-lg">Get started</a>
      <a href="/docs/patterns" class="af-btn af-is-outline af-is-lg">Browse patterns</a>
    </div>
  </div>
</section>

Feature grid

Three-up feature cards. Each card pairs a numbered icon box with a title and a one-line value proposition. Collapses to a single column below 768px.

Why Airframe

Primitives that outlive your stack

No runtime

Classes on markup. No provider, no hydration, nothing to ship at runtime.

Accessible defaults

Focus rings, contrast and hit areas come from the tokens, not from a checklist.

Themed by tokens

Override a handful of custom properties and every pattern follows.

<section class="af-section">
  <div class="af-container af-stack af-gap-xl">
    <hgroup class="af-stack af-gap-sm">
      <p class="af-eyebrow">Why Airframe</p>
      <h2>Primitives that outlive your stack</h2>
    </hgroup>
    <div class="af-grid-1 af-grid-3@md af-gap-lg">
      <article class="af-card">
        <span class="af-icon-box" aria-hidden="true">01</span>
        <h3 class="af-card__title">No runtime</h3>
        <p class="af-card__body">Classes on markup. No provider, no hydration, nothing to ship at runtime.</p>
      </article>
      <article class="af-card">
        <span class="af-icon-box" aria-hidden="true">02</span>
        <h3 class="af-card__title">Accessible defaults</h3>
        <p class="af-card__body">Focus rings, contrast and hit areas come from the tokens, not from a checklist.</p>
      </article>
      <article class="af-card">
        <span class="af-icon-box" aria-hidden="true">03</span>
        <h3 class="af-card__title">Themed by tokens</h3>
        <p class="af-card__body">Override a handful of custom properties and every pattern follows.</p>
      </article>
    </div>
  </div>
</section>

Pricing tiers

Three-tier pricing grid with the middle plan emphasised by a primary border and a chip. Prices use af-stat-display; each card footer carries a full-width action.

Pricing

Plans that scale with the team

Starter

For side projects

$0

  • Up to 3 projects
  • Community support
  • 7-day history

Team

Most popular
For product teams

$29 / user / month

  • Unlimited projects
  • Shared environments
  • Priority support

Enterprise

For regulated rollouts

Custom

  • SSO and audit log
  • Dedicated environment
  • Named support engineer
<section class="af-section">
  <div class="af-container af-stack af-gap-xl">
    <hgroup class="af-stack af-gap-sm af-items-center af-text-center">
      <p class="af-eyebrow">Pricing</p>
      <h2>Plans that scale with the team</h2>
    </hgroup>
    <div class="af-grid-1 af-grid-3@md af-gap-lg af-items-start">
      <article class="af-card">
        <div class="af-card__header">
          <h3 class="af-card__title">Starter</h3>
          <span class="af-card__subtitle">For side projects</span>
        </div>
        <p class="af-stat-display">$0</p>
        <ul class="af-stack-sm">
          <li>
            <span aria-hidden="true">&check;</span>
            Up to 3 projects
          </li>
          <li>
            <span aria-hidden="true">&check;</span>
            Community support
          </li>
          <li>
            <span aria-hidden="true">&check;</span>
            7-day history
          </li>
        </ul>
        <div class="af-card__footer">
          <a href="/signup" class="af-btn af-is-outline af-w-full">Start free</a>
        </div>
      </article>
      <article class="af-card af-border-primary">
        <div class="af-card__header">
          <div class="af-inline af-justify-between af-items-center">
            <h3 class="af-card__title">Team</h3>
            <span class="af-chip">Most popular</span>
          </div>
          <span class="af-card__subtitle">For product teams</span>
        </div>
        <p class="af-stat-display">
          $29
          <span class="af-text-sm af-text-muted">/ user / month</span>
        </p>
        <ul class="af-stack-sm">
          <li>
            <span aria-hidden="true">&check;</span>
            Unlimited projects
          </li>
          <li>
            <span aria-hidden="true">&check;</span>
            Shared environments
          </li>
          <li>
            <span aria-hidden="true">&check;</span>
            Priority support
          </li>
        </ul>
        <div class="af-card__footer">
          <a href="/signup" class="af-btn af-w-full">Start 14-day trial</a>
        </div>
      </article>
      <article class="af-card">
        <div class="af-card__header">
          <h3 class="af-card__title">Enterprise</h3>
          <span class="af-card__subtitle">For regulated rollouts</span>
        </div>
        <p class="af-stat-display">Custom</p>
        <ul class="af-stack-sm">
          <li>
            <span aria-hidden="true">&check;</span>
            SSO and audit log
          </li>
          <li>
            <span aria-hidden="true">&check;</span>
            Dedicated environment
          </li>
          <li>
            <span aria-hidden="true">&check;</span>
            Named support engineer
          </li>
        </ul>
        <div class="af-card__footer">
          <a href="/contact" class="af-btn af-is-outline af-w-full">Talk to sales</a>
        </div>
      </article>
    </div>
  </div>
</section>

Testimonials

Two-up quote cards using figure and blockquote, with an avatar-led attribution footer. Swap the initials avatar for an image when you have one.

Customers

What teams say after a week

We deleted 4,000 lines of utility classes and the design got more consistent, not less. The markup finally reads like the UI.
A. Rivera Platform lead
Two frameworks, one stylesheet. Our Angular admin and our React marketing site finally look like the same product.
J. Tanaka Design systems engineer
<section class="af-section">
  <div class="af-container af-stack af-gap-xl">
    <hgroup class="af-stack af-gap-sm">
      <p class="af-eyebrow">Customers</p>
      <h2>What teams say after a week</h2>
    </hgroup>
    <div class="af-grid-1 af-grid-2@md af-gap-lg">
      <figure class="af-card">
        <blockquote class="af-blockquote">We deleted 4,000 lines of utility classes and the design got more consistent, not less. The markup finally reads like the UI.</blockquote>
        <figcaption class="af-inline af-gap-sm af-items-center">
          <span class="af-avatar" aria-hidden="true">AR</span>
          <span class="af-stack af-gap-0">
            <span class="af-text-strong">A. Rivera</span>
            <span class="af-text-sm af-text-muted">Platform lead</span>
          </span>
        </figcaption>
      </figure>
      <figure class="af-card">
        <blockquote class="af-blockquote">Two frameworks, one stylesheet. Our Angular admin and our React marketing site finally look like the same product.</blockquote>
        <figcaption class="af-inline af-gap-sm af-items-center">
          <span class="af-avatar" aria-hidden="true">JT</span>
          <span class="af-stack af-gap-0">
            <span class="af-text-strong">J. Tanaka</span>
            <span class="af-text-sm af-text-muted">Design systems engineer</span>
          </span>
        </figcaption>
      </figure>
    </div>
  </div>
</section>

CTA banner

Full-width closing call to action locked to the catalog dark canvas. af-force-dark remaps surfaces and text; af-bg-background paints that canvas. Buttons keep the live brand.

Build the thing. Ship the thing.

One stylesheet, every framework, nothing to configure. Start with the install and add patterns as you need them.

<section class="af-section-lg af-force-dark af-bg-background">
  <div class="af-container af-container-md af-stack af-gap-lg af-items-center af-text-center">
    <h2 class="af-text-fluid-h2">Build the thing. Ship the thing.</h2>
    <p class="af-measure af-text-muted">One stylesheet, every framework, nothing to configure. Start with the install and add patterns as you need them.</p>
    <div class="af-inline af-justify-center af-wrap">
      <a href="/docs/installation" class="af-btn af-is-lg">Get started</a>
      <a href="/docs" class="af-btn af-is-secondary af-is-lg">Read the docs</a>
    </div>
  </div>
</section>

Logo cloud

Social-proof strip that sits between the hero and the feature grid. Wordmarks are plain list items here — drop in img elements with the same list structure when you have real logos.

Trusted by teams at

  • Delta
  • Vortex
  • Halcyon
  • Strake
  • Nimbus
<section class="af-section-sm">
  <div class="af-container af-stack af-gap-md af-items-center">
    <p class="af-eyebrow af-text-muted">Trusted by teams at</p>
    <ul class="af-inline af-gap-xl af-justify-center af-wrap">
      <li class="af-text-h4 af-text-muted">Delta</li>
      <li class="af-text-h4 af-text-muted">Vortex</li>
      <li class="af-text-h4 af-text-muted">Halcyon</li>
      <li class="af-text-h4 af-text-muted">Strake</li>
      <li class="af-text-h4 af-text-muted">Nimbus</li>
    </ul>
  </div>
</section>

FAQ accordion

Five-question FAQ built on native details and summary, so it expands with zero JavaScript and stays keyboard accessible by default.

FAQ

Common questions

Do I need a build step?

No. Import one stylesheet from npm or a CDN and start writing classes. There is no compiler, no config file and no purge step.

Which frameworks does it support?

All of them. Airframe is CSS plus markup conventions, so the same class strings work in React, Vue, Angular, Svelte, Rails or a static HTML file. React uses className instead of class.

Do I need Tailwind or an icon library?

Neither. Every block on this page uses only Airframe classes and plain HTML, so pasted markup renders the same without adding a single dependency.

How do I match our brand?

Override the design tokens. Setting a handful of custom properties such as --af-color-primary and --af-radius re-themes every pattern at once, with no rebuild.

Will it fight my existing CSS?

Airframe ships inside CSS cascade layers with low specificity, so your own rules win by default and you can adopt it one page at a time.

<section class="af-section">
  <div class="af-container af-container-md af-stack af-gap-xl">
    <hgroup class="af-stack af-gap-sm">
      <p class="af-eyebrow">FAQ</p>
      <h2>Common questions</h2>
    </hgroup>
    <div class="af-stack af-gap-sm">
      <details class="af-accordion af-has-slide">
        <summary>Do I need a build step?</summary>
        <div class="af-accordion-content">
          <p>No. Import one stylesheet from npm or a CDN and start writing classes. There is no compiler, no config file and no purge step.</p>
        </div>
      </details>
      <details class="af-accordion af-has-slide">
        <summary>Which frameworks does it support?</summary>
        <div class="af-accordion-content">
          <p>All of them. Airframe is CSS plus markup conventions, so the same class strings work in React, Vue, Angular, Svelte, Rails or a static HTML file. React uses className instead of class.</p>
        </div>
      </details>
      <details class="af-accordion af-has-slide">
        <summary>Do I need Tailwind or an icon library?</summary>
        <div class="af-accordion-content">
          <p>Neither. Every block on this page uses only Airframe classes and plain HTML, so pasted markup renders the same without adding a single dependency.</p>
        </div>
      </details>
      <details class="af-accordion af-has-slide">
        <summary>How do I match our brand?</summary>
        <div class="af-accordion-content">
          <p>Override the design tokens. Setting a handful of custom properties such as --af-color-primary and --af-radius re-themes every pattern at once, with no rebuild.</p>
        </div>
      </details>
      <details class="af-accordion af-has-slide">
        <summary>Will it fight my existing CSS?</summary>
        <div class="af-accordion-content">
          <p>Airframe ships inside CSS cascade layers with low specificity, so your own rules win by default and you can adopt it one page at a time.</p>
        </div>
      </details>
    </div>
  </div>
</section>

Newsletter signup

Single-field email capture in a card. The input and button sit in a 12-column grid so they share a row from 640px up and stack on phones.

Release notes

One email per release. Nothing else.

Unsubscribe in one click. We never share your address.

<section class="af-section">
  <div class="af-container af-container-sm">
    <form class="af-card" action="/subscribe" method="post">
      <div class="af-card__header">
        <h2 class="af-card__title">Release notes</h2>
        <span class="af-card__subtitle">One email per release. Nothing else.</span>
      </div>
      <div class="af-field">
        <label class="af-field__label" for="newsletter-email">Email</label>
        <div class="af-grid af-gap-sm">
          <div class="af-col-span-12 af-col-span-8@sm">
            <input type="email" id="newsletter-email" name="email" class="af-input" placeholder="you@example.com" autocomplete="email" required>
          </div>
          <div class="af-col-span-12 af-col-span-4@sm">
            <button type="submit" class="af-btn af-w-full">Subscribe</button>
          </div>
        </div>
        <p class="af-field__help">Unsubscribe in one click. We never share your address.</p>
      </div>
    </form>
  </div>
</section>

Blog card grid

Three-up article grid. Each card is an <article> whose title wraps the link, so the whole row of headings reads as a list of destinations rather than a wall of "read more".

Writing

From the blog

Layers let a design system ship defaults your own CSS can override without a specificity war.

What we changed after watching keyboard users try to check out on three real storefronts.

Custom properties all the way down means a theme is a stylesheet, not a compile target.

<section class="af-section">
  <div class="af-container af-stack af-gap-xl">
    <hgroup class="af-stack af-gap-sm">
      <p class="af-eyebrow">Writing</p>
      <h2>From the blog</h2>
    </hgroup>
    <div class="af-grid-1 af-grid-3@md af-gap-lg">
      <article class="af-card af-has-hover">
        <div class="af-card__header">
          <span class="af-card__overline">Engineering</span>
          <h3 class="af-card__title">
            <a href="/blog/cascade-layers">Why we bet on cascade layers</a>
          </h3>
        </div>
        <p class="af-card__body">Layers let a design system ship defaults your own CSS can override without a specificity war.</p>
        <div class="af-card__footer af-inline af-gap-sm af-items-center">
          <span class="af-avatar af-avatar-sm" aria-hidden="true">AR</span>
          <span class="af-text-sm af-text-muted">A. Rivera &middot; 6 min read</span>
        </div>
      </article>
      <article class="af-card af-has-hover">
        <div class="af-card__header">
          <span class="af-card__overline">Accessibility</span>
          <h3 class="af-card__title">
            <a href="/blog/focus-visible">Focus rings are a feature</a>
          </h3>
        </div>
        <p class="af-card__body">What we changed after watching keyboard users try to check out on three real storefronts.</p>
        <div class="af-card__footer af-inline af-gap-sm af-items-center">
          <span class="af-avatar af-avatar-sm" aria-hidden="true">JT</span>
          <span class="af-text-sm af-text-muted">J. Tanaka &middot; 9 min read</span>
        </div>
      </article>
      <article class="af-card af-has-hover">
        <div class="af-card__header">
          <span class="af-card__overline">Product</span>
          <h3 class="af-card__title">
            <a href="/blog/theme-studio">Theming without a build step</a>
          </h3>
        </div>
        <p class="af-card__body">Custom properties all the way down means a theme is a stylesheet, not a compile target.</p>
        <div class="af-card__footer af-inline af-gap-sm af-items-center">
          <span class="af-avatar af-avatar-sm" aria-hidden="true">MO</span>
          <span class="af-text-sm af-text-muted">M. Okafor &middot; 4 min read</span>
        </div>
      </article>
    </div>
  </div>
</section>

Article body

Blog essay: chips and byline, a lead, a pull quote, and a code sample in the same measure as the prose.

Engineering 6 min read

Why the board should look like the deploy

A. Rivera 12 March 2026

If a card says done and production still runs last Tuesday’s build, the two systems do not share a clock.

If the board cannot point at the build, it is a mood board.

if (card.status === "done" && deploy.sha !== card.sha) {
  throw new Error("board is ahead of production");
}
<article class="af-stack af-gap-xl">
  <header class="af-stack af-gap-md">
    <div class="af-inline af-gap-sm">
      <span class="af-chip">Engineering</span>
      <span class="af-text-sm af-text-muted">6 min read</span>
    </div>
    <h1 class="af-text-fluid-h2">Why the board should look like the deploy</h1>
    <div class="af-inline af-gap-sm af-items-center">
      <span class="af-avatar" aria-hidden="true">AR</span>
      <span class="af-stack af-gap-0">
        <span class="af-text-strong">A. Rivera</span>
        <span class="af-text-sm af-text-muted">12 March 2026</span>
      </span>
    </div>
  </header>
  <div class="af-prose af-stack af-gap-md">
    <p class="af-lead">If a card says done and production still runs last Tuesday’s build, the two systems do not share a clock.</p>
    <blockquote class="af-blockquote">
      <p>If the board cannot point at the build, it is a mood board.</p>
    </blockquote>
  </div>
  <pre class="af-code-block">
    <code>if (card.status === "done" && deploy.sha !== card.sha) { throw new Error("board is ahead of production"); }</code>
  </pre>
</article>

Authentication

Sign-in and account entry points.

Sign-in card

Narrow sign-in card with labelled email and password fields, a remember-me checkbox paired with a recovery link, and a full-width submit.

Sign in

Welcome back
Forgot password?
<section class="af-section-lg af-bg-surface-secondary">
  <div class="af-container-xs">
    <div class="af-card">
      <div class="af-card__header">
        <h1 class="af-card__title">Sign in</h1>
        <span class="af-card__subtitle">Welcome back</span>
      </div>
      <form class="af-stack af-gap-lg" action="/session" method="post">
        <div class="af-field">
          <label class="af-field__label" for="signin-email">Email</label>
          <input type="email" id="signin-email" name="email" class="af-input" autocomplete="email" required>
        </div>
        <div class="af-field">
          <label class="af-field__label" for="signin-password">Password</label>
          <input type="password" id="signin-password" name="password" class="af-input" autocomplete="current-password" required>
        </div>
        <div class="af-inline af-justify-between af-wrap">
          <label>
            <input type="checkbox" name="remember" class="af-checkbox">
            <span>Remember me</span>
          </label>
          <a href="/password/reset" class="af-link af-text-sm">Forgot password?</a>
        </div>
        <button type="submit" class="af-btn af-w-full">Sign in</button>
      </form>
      <div class="af-card__footer af-is-muted af-text-center">
        <p class="af-text-sm">
          No account?
          <a href="/signup" class="af-link">Create one</a>
        </p>
      </div>
    </div>
  </div>
</section>

Create account card

Registration card with autocomplete tokens that let password managers do their job, a help line stating the password rule before submission, and a required terms checkbox.

Create your account

Free for 14 days, no card required

At least 12 characters.

<section class="af-section-lg af-bg-surface-secondary">
  <div class="af-container-xs">
    <div class="af-card">
      <div class="af-card__header">
        <h1 class="af-card__title">Create your account</h1>
        <span class="af-card__subtitle">Free for 14 days, no card required</span>
      </div>
      <form class="af-stack af-gap-lg" action="/accounts" method="post">
        <div class="af-field">
          <label class="af-field__label" for="signup-name">Full name</label>
          <input type="text" id="signup-name" name="name" class="af-input" autocomplete="name" required>
        </div>
        <div class="af-field">
          <label class="af-field__label" for="signup-email">Work email</label>
          <input type="email" id="signup-email" name="email" class="af-input" autocomplete="email" required>
        </div>
        <div class="af-field">
          <label class="af-field__label" for="signup-password">Password</label>
          <input type="password" id="signup-password" name="password" class="af-input" autocomplete="new-password" minlength="12" required aria-describedby="signup-password-help">
          <p class="af-field__help" id="signup-password-help">At least 12 characters.</p>
        </div>
        <label>
          <input type="checkbox" name="terms" class="af-checkbox" required>
          <span>
            I agree to the
            <a href="/terms" class="af-link">terms</a>
            and
            <a href="/privacy" class="af-link">privacy policy</a>
            .
          </span>
        </label>
        <button type="submit" class="af-btn af-w-full">Create account</button>
      </form>
      <div class="af-card__footer af-is-muted af-text-center">
        <p class="af-text-sm">
          Already have an account?
          <a href="/signin" class="af-link">Sign in</a>
        </p>
      </div>
    </div>
  </div>
</section>

Application

Dashboard and collection-view furniture.

Page heading with actions

The bar that opens almost every application screen: breadcrumb trail, h1, status and metadata, and the page-level actions. Current page is plain text with aria-current, not a link back to itself.

Deployments

Live Sydney · updated 4 minutes ago by A. Rivera
<div class="af-stack af-gap-md">
  <nav aria-label="Breadcrumb">
    <ol class="af-breadcrumbs">
      <li class="af-breadcrumbs-item">
        <a href="/projects" class="af-breadcrumbs-link">Projects</a>
      </li>
      <li class="af-breadcrumbs-item">
        <a href="/projects/atlas" class="af-breadcrumbs-link">Atlas</a>
      </li>
      <li class="af-breadcrumbs-item" aria-current="page">Deployments</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 class="af-text-h2">Deployments</h1>
      <div class="af-inline af-gap-sm af-items-center af-wrap">
        <span class="af-badge af-is-success">Live</span>
        <span class="af-text-sm af-text-muted">Sydney &middot; updated 4 minutes ago by A. Rivera</span>
      </div>
    </div>
    <div class="af-inline af-gap-sm af-wrap">
      <button type="button" class="af-btn af-is-outline af-is-secondary">View logs</button>
      <button type="button" class="af-btn">Deploy</button>
    </div>
  </div>
</div>

Stats row

Four-tile KPI row for a dashboard header. af-kpi already draws its own surface, so the tiles go straight into the grid rather than being wrapped in cards.

Overview

This month

Updated 12 minutes ago

Monthly active users
48,294
+12.4% vs last month
Sessions today
3,128
+5.2% vs average
Conversion rate
4.7%
-0.3% vs last week
Response time
182ms
-8.1% vs last week
<section class="af-stack af-gap-lg">
  <div class="af-inline af-justify-between af-items-end af-wrap">
    <hgroup class="af-stack af-gap-xs">
      <p class="af-eyebrow">Overview</p>
      <h2 class="af-text-h3">This month</h2>
    </hgroup>
    <p class="af-text-sm af-text-muted">Updated 12 minutes ago</p>
  </div>
  <div class="af-grid-1 af-grid-2@sm af-grid-4@lg af-gap-lg">
    <div class="af-kpi">
      <div class="af-kpi-label">Monthly active users</div>
      <div class="af-kpi-value">48,294</div>
      <div class="af-kpi-change af-is-positive">+12.4% vs last month</div>
    </div>
    <div class="af-kpi">
      <div class="af-kpi-label">Sessions today</div>
      <div class="af-kpi-value">3,128</div>
      <div class="af-kpi-change af-is-positive">+5.2% vs average</div>
    </div>
    <div class="af-kpi">
      <div class="af-kpi-label">Conversion rate</div>
      <div class="af-kpi-value">4.7%</div>
      <div class="af-kpi-change af-is-negative">-0.3% vs last week</div>
    </div>
    <div class="af-kpi">
      <div class="af-kpi-label">Response time</div>
      <div class="af-kpi-value">182ms</div>
      <div class="af-kpi-change af-is-positive">-8.1% vs last week</div>
    </div>
  </div>
</section>

Empty state panel

Card-wrapped empty state for a collection view that has no rows yet. Pairs a primary action with a lower-commitment alternative.

Projects

Everything you ship lives here

No projects yet

Create a project to start tracking deploys, environments and members.

<div class="af-card">
  <div class="af-card__header">
    <h2 class="af-card__title">Projects</h2>
    <span class="af-card__subtitle">Everything you ship lives here</span>
  </div>
  <div class="af-empty-state">
    <div class="af-empty-state-icon" aria-hidden="true">&#128193;</div>
    <h3 class="af-empty-state-title">No projects yet</h3>
    <p class="af-text-muted af-measure-narrow">Create a project to start tracking deploys, environments and members.</p>
    <div class="af-inline af-justify-center af-wrap">
      <button type="button" class="af-btn">Create project</button>
      <button type="button" class="af-btn af-is-secondary">Import from template</button>
    </div>
  </div>
</div>

Table with toolbar and pagination

The workhorse collection view: card header as toolbar, a real <table> with scope on every header cell, and pagination in the footer. af-table-responsive only scrolls the table, so the toolbar and pagination stay put.

Members

24 people can access this workspace
Workspace members, sorted by name
Name Role Status Last active Actions
A. Rivera Admin Active 2 hours ago
J. Tanaka Editor Invited Never
M. Okafor Viewer Active Yesterday
<div class="af-card">
  <div class="af-card__header af-grid af-gap-md af-items-center">
    <div class="af-col-span-12 af-col-span-6@md af-stack af-gap-xs">
      <h2 class="af-card__title">Members</h2>
      <span class="af-card__subtitle">24 people can access this workspace</span>
    </div>
    <div class="af-col-span-12 af-col-span-6@md af-grid af-gap-sm">
      <div class="af-col-span-8">
        <input type="search" id="members-search" name="q" class="af-input" placeholder="Search members" aria-label="Search members">
      </div>
      <div class="af-col-span-4">
        <button type="button" class="af-btn af-w-full">Invite</button>
      </div>
    </div>
  </div>
  <div class="af-table-responsive">
    <table class="af-table">
      <caption class="af-sr-only">Workspace members, sorted by name</caption>
      <thead>
        <tr>
          <th scope="col">Name</th>
          <th scope="col">Role</th>
          <th scope="col">Status</th>
          <th scope="col">Last active</th>
          <th scope="col">
            <span class="af-sr-only">Actions</span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>A. Rivera</td>
          <td>Admin</td>
          <td>
            <span class="af-badge af-is-success">Active</span>
          </td>
          <td>2 hours ago</td>
          <td>
            <button type="button" class="af-btn af-is-sm af-is-ghost">Edit</button>
          </td>
        </tr>
        <tr>
          <td>J. Tanaka</td>
          <td>Editor</td>
          <td>
            <span class="af-badge af-is-warning">Invited</span>
          </td>
          <td>Never</td>
          <td>
            <button type="button" class="af-btn af-is-sm af-is-ghost">Edit</button>
          </td>
        </tr>
        <tr>
          <td>M. Okafor</td>
          <td>Viewer</td>
          <td>
            <span class="af-badge af-is-success">Active</span>
          </td>
          <td>Yesterday</td>
          <td>
            <button type="button" class="af-btn af-is-sm af-is-ghost">Edit</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
  <div class="af-card__footer af-inline af-justify-between af-items-center af-wrap af-gap-md">
    <p class="af-text-sm af-text-muted">Showing 1&ndash;3 of 24</p>
    <nav aria-label="Pagination">
      <ul class="af-pagination">
        <li class="af-pagination-item">
          <a href="#" class="af-pagination-link" aria-disabled="true">Previous</a>
        </li>
        <li class="af-pagination-item">
          <a href="?page=1" class="af-pagination-link" aria-current="page">1</a>
        </li>
        <li class="af-pagination-item">
          <a href="?page=2" class="af-pagination-link">2</a>
        </li>
        <li class="af-pagination-item">
          <a href="?page=3" class="af-pagination-link">3</a>
        </li>
        <li class="af-pagination-item">
          <a href="?page=2" class="af-pagination-link">Next</a>
        </li>
      </ul>
    </nav>
  </div>
</div>

Record list with row actions

Stacked list where every row carries metadata and its own controls. af-action-row-group draws the dividers, so the rows need no borders of their own — and no interactive element ends up nested inside another.

Pending invitations

Invited, but not signed up yet

j.tanaka@example.com

Editor · invited 3 days ago by A. Rivera

m.okafor@example.com

Viewer · invited yesterday by A. Rivera

s.mehta@example.com

Admin · invite expires in 2 days

<div class="af-card">
  <div class="af-card__header">
    <h2 class="af-card__title">Pending invitations</h2>
    <span class="af-card__subtitle">Invited, but not signed up yet</span>
  </div>
  <div class="af-action-row-group">
    <div class="af-action-row">
      <div class="af-action-row__copy">
        <h3 class="af-action-row__title">j.tanaka@example.com</h3>
        <p class="af-action-row__hint">Editor &middot; invited 3 days ago by A. Rivera</p>
      </div>
      <div class="af-action-row__control af-inline af-gap-sm">
        <button type="button" class="af-btn af-is-sm af-is-outline af-is-secondary">Resend</button>
        <button type="button" class="af-btn af-is-sm af-is-ghost af-is-danger">Revoke</button>
      </div>
    </div>
    <div class="af-action-row">
      <div class="af-action-row__copy">
        <h3 class="af-action-row__title">m.okafor@example.com</h3>
        <p class="af-action-row__hint">Viewer &middot; invited yesterday by A. Rivera</p>
      </div>
      <div class="af-action-row__control af-inline af-gap-sm">
        <button type="button" class="af-btn af-is-sm af-is-outline af-is-secondary">Resend</button>
        <button type="button" class="af-btn af-is-sm af-is-ghost af-is-danger">Revoke</button>
      </div>
    </div>
    <div class="af-action-row">
      <div class="af-action-row__copy">
        <h3 class="af-action-row__title">s.mehta@example.com</h3>
        <p class="af-action-row__hint">Admin &middot; invite expires in 2 days</p>
      </div>
      <div class="af-action-row__control af-inline af-gap-sm">
        <button type="button" class="af-btn af-is-sm af-is-outline af-is-secondary">Resend</button>
        <button type="button" class="af-btn af-is-sm af-is-ghost af-is-danger">Revoke</button>
      </div>
    </div>
  </div>
</div>

Detail panel

Record summary for a detail screen. A description list is the right element here: af-dl lays the pairs out in two columns while dt/dd keep the label-to-value relationship in the accessibility tree.

Invoice INV-2043

Issued 12 March 2026
Awaiting payment
Customer
Acme Co
Billing email
ap@example.com
Plan
Team · 24 seats
Amount due
$696.00 AUD
Due date
26 March 2026
<div class="af-card">
  <div class="af-card__header af-inline af-justify-between af-items-start af-wrap af-gap-md">
    <div class="af-stack af-gap-xs">
      <h2 class="af-card__title">Invoice INV-2043</h2>
      <span class="af-card__subtitle">Issued 12 March 2026</span>
    </div>
    <span class="af-badge af-is-warning">Awaiting payment</span>
  </div>
  <dl class="af-dl">
    <dt>Customer</dt>
    <dd>Acme Co</dd>
    <dt>Billing email</dt>
    <dd>
      <a href="mailto:ap@example.com" class="af-link">ap@example.com</a>
    </dd>
    <dt>Plan</dt>
    <dd>Team &middot; 24 seats</dd>
    <dt>Amount due</dt>
    <dd>$696.00 AUD</dd>
    <dt>Due date</dt>
    <dd>26 March 2026</dd>
  </dl>
  <div class="af-card__footer af-inline af-gap-sm af-wrap">
    <button type="button" class="af-btn">Record payment</button>
    <button type="button" class="af-btn af-is-outline af-is-secondary">Download PDF</button>
  </div>
</div>

Confirmation dialog

Destructive-action confirmation on the native <dialog> element, so Escape, focus handling and the backdrop come from the browser — the two inline handlers are the whole integration. The preview is the same markup left open inside a positioned box, since an open non-modal dialog is absolutely positioned.

Delete Atlas?

Atlas and its 14 deployments will be removed permanently. Members lose access immediately and this cannot be undone.

<button type="button" class="af-btn af-is-danger" onclick="document.getElementById('confirm-delete').showModal()">Delete project</button>

<dialog class="af-dialog" id="confirm-delete" aria-labelledby="confirm-delete-title">
  <div class="af-dialog-header">
    <h2 class="af-dialog-title" id="confirm-delete-title">Delete Atlas?</h2>
    <button type="button" class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close" onclick="this.closest('dialog').close()">&times;</button>
  </div>
  <div class="af-dialog-body">
    <p>Atlas and its 14 deployments will be removed permanently. Members lose access immediately and this cannot be undone.</p>
  </div>
  <div class="af-dialog-footer">
    <button type="button" class="af-btn af-is-outline af-is-secondary" onclick="this.closest('dialog').close()">Cancel</button>
    <button type="button" class="af-btn af-is-danger">Delete project</button>
  </div>
</dialog>

Inbox list

Selectable message rows. af-list-group draws the borders; the active item is the one currently open in the reading pane.

  • Delta billing 10:14
    Invoice INV-2043 is awaiting payment
  • J. Tanaka 2
    Preview deploys for Atlas look good
  • Vortex support Mon
    Ticket #4821 — cache invalidation
<ul class="af-list-group">
  <li class="af-list-group-item af-is-active af-stack af-gap-xs af-items-start">
    <div class="af-spread af-w-full">
      <span class="af-text-strong">Delta billing</span>
      <span class="af-text-sm af-text-muted">10:14</span>
    </div>
    <span class="af-text-sm">Invoice INV-2043 is awaiting payment</span>
  </li>
  <li class="af-list-group-item af-stack af-gap-xs af-items-start">
    <div class="af-spread af-w-full">
      <span class="af-text-strong">J. Tanaka</span>
      <span class="af-badge">2</span>
    </div>
    <span class="af-text-sm af-text-muted">Preview deploys for Atlas look good</span>
  </li>
  <li class="af-list-group-item af-stack af-gap-xs af-items-start">
    <div class="af-spread af-w-full">
      <span class="af-text-strong">Vortex support</span>
      <span class="af-text-sm af-text-muted">Mon</span>
    </div>
    <span class="af-text-sm af-text-muted">Ticket #4821 — cache invalidation</span>
  </li>
</ul>

Kanban board

Three columns in a horizontal row that scrolls on small screens. Each card carries a label chip and an assignee avatar — no drag library required for the layout.

Todo · 2

SSO notes on invite email

Docs

Sydney region runbook

Ops

In review · 1

Cache invalidation on tag

Bug

Done · 1

Ship Atlas 1.4

Release
<div class="af-inline af-nowrap af-items-start af-gap-lg af-overflow-x-auto">
  <section class="af-stack af-gap-md" style="flex: 0 0 16rem" aria-label="Todo">
    <h2 class="af-text-sm af-text-muted">Todo · 2</h2>
    <article class="af-card">
      <h3 class="af-card__title">SSO notes on invite email</h3>
      <div class="af-inline af-gap-sm">
        <span class="af-badge">Docs</span>
        <span class="af-avatar af-avatar-sm" aria-hidden="true">MO</span>
      </div>
    </article>
    <article class="af-card">
      <h3 class="af-card__title">Sydney region runbook</h3>
      <div class="af-inline af-gap-sm">
        <span class="af-badge af-is-info">Ops</span>
        <span class="af-avatar af-avatar-sm" aria-hidden="true">AR</span>
      </div>
    </article>
  </section>
  <section class="af-stack af-gap-md" style="flex: 0 0 16rem" aria-label="In review">
    <h2 class="af-text-sm af-text-muted">In review · 1</h2>
    <article class="af-card af-border-primary">
      <h3 class="af-card__title">Cache invalidation on tag</h3>
      <div class="af-inline af-gap-sm">
        <span class="af-badge af-is-danger">Bug</span>
        <span class="af-avatar af-avatar-sm" aria-hidden="true">JT</span>
      </div>
    </article>
  </section>
  <section class="af-stack af-gap-md" style="flex: 0 0 16rem" aria-label="Done">
    <h2 class="af-text-sm af-text-muted">Done · 1</h2>
    <article class="af-card">
      <h3 class="af-card__title">Ship Atlas 1.4</h3>
      <div class="af-inline af-gap-sm">
        <span class="af-badge af-is-success">Release</span>
        <span class="af-avatar af-avatar-sm" aria-hidden="true">MO</span>
      </div>
    </article>
  </section>
</div>

Chat thread

Incoming and outgoing bubbles in one column. Own messages align to the end and use the primary fill; the other person stays on the surface.

J. Tanaka Online

Preview still serves yesterday’s HTML when I move the tag.

Checking whether we purge by URL or by tag.

<section class="af-stack af-gap-md">
  <header class="af-inline af-gap-sm af-items-center">
    <span class="af-avatar af-avatar-sm" aria-hidden="true">JT</span>
    <span class="af-stack af-gap-0">
      <span class="af-text-strong">J. Tanaka</span>
      <span class="af-text-sm af-text-muted">Online</span>
    </span>
  </header>
  <div class="af-stack af-gap-md">
    <div class="af-inline af-justify-start">
      <p class="af-card" style="max-width: 42ch">Preview still serves yesterday’s HTML when I move the tag.</p>
    </div>
    <div class="af-inline af-justify-end">
      <p class="af-card af-bg-primary af-text-on-primary" style="max-width: 42ch">Checking whether we purge by URL or by tag.</p>
    </div>
  </div>
  <form class="af-inline af-gap-sm af-items-end" action="/messages" method="post">
    <label class="af-sr-only" for="chat-compose">Message</label>
    <input id="chat-compose" name="body" class="af-input af-grow" placeholder="Message J. Tanaka">
    <button type="submit" class="af-btn">Send</button>
  </form>
</section>

Forms

Multi-field capture with real field semantics.

Contact form

Two-column contact section: an intro panel with a description list of contact details, and a card-wrapped form whose fields share a 12-column grid.

Contact

Talk to the team

Sales questions, technical help or partnerships. A real person answers every message.

Email
hello@example.com
Response time
One business day

Tell us a little about what you are building.

<section class="af-section">
  <div class="af-container">
    <div class="af-grid af-gap-2xl af-items-start">
      <div class="af-col-span-12 af-col-span-5@lg af-stack af-gap-md">
        <p class="af-eyebrow">Contact</p>
        <h2>Talk to the team</h2>
        <p class="af-text-muted">Sales questions, technical help or partnerships. A real person answers every message.</p>
        <dl class="af-dl">
          <dt>Email</dt>
          <dd>
            <a href="mailto:hello@example.com" class="af-link">hello@example.com</a>
          </dd>
          <dt>Response time</dt>
          <dd>One business day</dd>
        </dl>
      </div>
      <div class="af-col-span-12 af-col-span-7@lg">
        <form class="af-card" action="/contact" method="post">
          <div class="af-grid af-gap-md">
            <div class="af-col-span-12 af-col-span-6@sm">
              <div class="af-field">
                <label class="af-field__label" for="contact-name">Name</label>
                <input type="text" id="contact-name" name="name" class="af-input" autocomplete="name" required>
              </div>
            </div>
            <div class="af-col-span-12 af-col-span-6@sm">
              <div class="af-field">
                <label class="af-field__label" for="contact-email">Email</label>
                <input type="email" id="contact-email" name="email" class="af-input" autocomplete="email" required>
              </div>
            </div>
            <div class="af-col-span-12">
              <div class="af-field">
                <label class="af-field__label" for="contact-topic">Topic</label>
                <select id="contact-topic" name="topic" class="af-select">
                  <option>Sales</option>
                  <option>Technical support</option>
                  <option>Partnerships</option>
                </select>
              </div>
            </div>
            <div class="af-col-span-12">
              <div class="af-field">
                <label class="af-field__label" for="contact-message">Message</label>
                <textarea id="contact-message" name="message" class="af-textarea" rows="4" required></textarea>
                <p class="af-field__help">Tell us a little about what you are building.</p>
              </div>
            </div>
          </div>
          <div class="af-card__footer af-inline af-justify-between af-items-center af-wrap">
            <p class="af-text-sm af-text-muted">We reply within one business day.</p>
            <button type="submit" class="af-btn">Send message</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</section>

Settings form with sections

Long settings form split into fieldsets with legends, so each group announces itself. Text fields share a 12-column grid; the toggles use action rows, where every switch gets its label from the row title.

Workspace settings

These apply to everyone in the workspace
Details

Shown to new members when they accept an invitation.

Notifications

Deploy alerts

Email the team when a deploy fails

Weekly digest

Usage and billing summary every Monday

<form class="af-card" action="/settings" method="post">
  <div class="af-card__header">
    <h2 class="af-card__title">Workspace settings</h2>
    <span class="af-card__subtitle">These apply to everyone in the workspace</span>
  </div>

  <fieldset>
    <legend>Details</legend>
    <div class="af-grid af-gap-md">
      <div class="af-col-span-12 af-col-span-6@sm">
        <div class="af-field">
          <label class="af-field__label" for="settings-name">Workspace name</label>
          <input type="text" id="settings-name" name="name" class="af-input" value="Airframe" required>
        </div>
      </div>
      <div class="af-col-span-12 af-col-span-6@sm">
        <div class="af-field">
          <label class="af-field__label" for="settings-region">Primary region</label>
          <select id="settings-region" name="region" class="af-select">
            <option>Sydney</option>
            <option>Frankfurt</option>
            <option>Oregon</option>
          </select>
        </div>
      </div>
      <div class="af-col-span-12">
        <div class="af-field">
          <label class="af-field__label" for="settings-description">Description</label>
          <textarea id="settings-description" name="description" class="af-textarea" rows="3"></textarea>
          <p class="af-field__help">Shown to new members when they accept an invitation.</p>
        </div>
      </div>
    </div>
  </fieldset>

  <fieldset>
    <legend>Notifications</legend>
    <div class="af-action-row-group">
      <div class="af-action-row">
        <div class="af-action-row__copy">
          <h3 class="af-action-row__title">Deploy alerts</h3>
          <p class="af-action-row__hint">Email the team when a deploy fails</p>
        </div>
        <div class="af-action-row__control">
          <input type="checkbox" role="switch" id="settings-deploy-alerts" name="deploy-alerts" aria-label="Deploy alerts" checked>
        </div>
      </div>
      <div class="af-action-row">
        <div class="af-action-row__copy">
          <h3 class="af-action-row__title">Weekly digest</h3>
          <p class="af-action-row__hint">Usage and billing summary every Monday</p>
        </div>
        <div class="af-action-row__control">
          <input type="checkbox" role="switch" id="settings-digest" name="digest" aria-label="Weekly digest">
        </div>
      </div>
    </div>
  </fieldset>

  <div class="af-card__footer af-inline af-justify-end af-gap-sm af-wrap">
    <button type="button" class="af-btn af-is-outline af-is-secondary">Cancel</button>
    <button type="submit" class="af-btn">Save changes</button>
  </div>
</form>

Need the page around the block?

Each block is a single section. For whole screens — landing, changelog, dashboard, inbox, board and settings — see the blueprints.