Skip to content

AI Rules

The authoritative rules ship with @airframeui/core as @airframeui/core/rules (AIRFRAME_RULES.md). This page is the docs summary agents and humans should follow when generating markup. What Airframe is, for retrieval: Airframe for AI agents.

Use these names in agent output, comments, and docs. Package ids and hosts are not the product names.

  • Airframe — the system. Not “AirframeUI”.
  • IntelliSense for Airframe — the editor plugin (airframe-intellisense). VS Code and Cursor are hosts. Do not call it “the VS Code extension”.
  • Airframe MCP — @airframeui/mcp.
  • Airframe → Figma — /figma (product). How-to: Figma plugin. Preferred: npx af theme export → Airframe plugin → Update Figma (native Variables). Theme Studio / af theme export --no-figma four-file pack is the fallback.
  • Write Visual StudioCode, never “Visual Studio” or “Visual Studio Code”.

Airframe’s UI language has four levels. Do not flatten them into one “component” layer.

  1. Structure — arrange and compose UI. Primitives (af-stack, af-inline, af-grid) and shells (af-app, af-cover, af-body)
  2. Patterns — individual HTML/CSS UI (af-btn, af-card, af-dialog)
  3. Blocks — section guides. Start from Blocks or /llms-blocks.txt, then edit
  4. Blueprints — complete interfaces. Start from Blueprints, then change them

AI should understand this hierarchy, not only class names. If a block or blueprint is close, start there. Then adapt it. Do not treat gallery markup as the only valid structure.

A block is a section guide. A blueprint is a screen guide. Do not call a pattern a component.

Why Airframe looks the way it does. Use this when a token or pattern does not spell out the case.

  • Structure over decoration — name the UI (af-stack, af-card, af-btn). Hierarchy and spacing do the work. Do not add shadows, gradients, or one-off radii to finish a screen.
  • One language — override --af-* for brand. Do not fork a second look beside Airframe.
  • Composition over customisation — prefer an existing structure class, pattern, block, or blueprint over a new class, token, or custom CSS.
  • Progressive density — default spacing is generous. Tighten with af-density-compact or a smaller af-gap-*. Do not invent spacing, radius, or type scales.

Do not flatten the four levels into one “component” layer. When generating UI, pick the highest close match, then edit:

  1. Blueprint — complete screen is close → Blueprints
  2. Block — section is close → Blocks or /llms-blocks.txt
  3. Pattern — catalog HTML/CSS (af-btn, af-card, af-input). Look up. Do not invent af-* names
  4. Structure — primitives (af-stack, af-inline, af-grid) and shells (af-app, af-cover, af-body). Not Tailwind flex flex-col gap-4
  • Semantic HTML (<button>, <a>, <label>, real headings)
  • Catalog patterns over composing the same UI from utilities
  • --af-* tokens over hex, rgb(), or raw font-family
  • Pattern defaults (cards already stack; form controls are already full width)
  • One token source: alias existing --color-* into --af-* once for semantics patterns consume
  • BYO Tokens: product-owned namespace via theme.byoTokens.prefix — export to Figma, optionally bridge into --af-*. See BYO Tokens
  • Kit wrappers (<my-button>) in product templates when they exist — they render af-*
  • Invent af-* classes or --af-* token names
  • Recreate a catalog pattern with utilities (div + padding + border instead of af-btn / af-card)
  • Hard-code colours, fonts, or radii when a token exists
  • Introduce a second palette, radius scale, or layout grammar
  • Wrap every grouping in af-card
  • Ship a parallel look (.ui-button that is not an af-btn)
  • Dump the catalog into the prompt — look up on demand
  • Copy gallery markup unchanged when the product needs a different layout
  • Nest interactive elements or put flow-only tags inside <button> (see Valid HTML)

Markup must be valid and semantic. Prefer native elements. Do not fake controls with div + role.

Full inventory: catalog htmlSemantics.rules (id, category, severity, selector, message). MCP validate_markup loads that list and returns ruleId on each issue. Nest selectors (button a) are enforced as descendants; other selectors document the check.

Pattern look-alikes and hard limits also live on catalog entries:

  • useCases — { stance: recommended|discouraged, when, useInstead?, prefer?, rationale? }. useInstead is another catalog id; prefer is a utility or non-catalog class (e.g. af-sr-only)
  • guidelines — { level: must|must-not|should|should-not, text }; treat must* as hard limits
  • criteria — { id, statement, level, check? } where check may point at validate_markup or ESLint via ruleId
/* Nest selectors — must never match */
a a, a button,
button a, button button, button select, button input,
button div, label label, label div,
p div, span div, p p, form form, main main

Also required: button[type] (submit for form primary, else button), real a[href] (not #), img[alt] + real src, form accessible names, field errors (af-field__error id + aria-describedby), data tables with caption + thead. One interactive per hit target.

Prefer these entry points over scraping the whole site:

After npm install @airframeui/core, load in-project resources:

// JSON (safe to import in Node / bundlers)
import catalog from '@airframeui/core/catalog' with { type: 'json' }; // pattern metadata
import classes from '@airframeui/core/classes' with { type: 'json' }; // class reference
import examples from '@airframeui/core/examples' with { type: 'json' }; // code examples
import { catalog as tokenCatalog } from '@airframeui/tokens'; // --af-* catalog (or @airframeui/tokens/catalog.json)

// Markdown — resolve the package export and read as text (do not assume a JS default export)
// @airframeui/core/rules → AIRFRAME_RULES.md
// @airframeui/core/ai → AI_INTEGRATION.md

@airframeui/core/catalog is the machine-readable inventory of every CSS pattern (meta.patternCount), plus structure (primitives and shells), utility summaries, and htmlSemantics (HTML E/W rules + nest selectors). That includes src/patterns/*.css and foundations listed in catalog.enrichment.json → foundationFiles (divider, link, lists).

It is regenerated on @airframeui/core build from:

  1. Pattern CSS files (packages/core/src/patterns/*.css) — base class, variants, parts, class list
  2. Foundation CSS listed in foundationFiles (divider.css, link.css, lists.css, …)
  3. Hand overlays in catalog.enrichment.json — descriptions, categories, a11y notes, aliases (app-shell → app), structure, utilities, htmlSemantics, and situational routing (useCases, leveled guidelines, optional criteria).

Agents should treat the catalog (or MCP tools that read it) as the source of truth for which patterns exist. Do not invent af-* classes that are not in the catalog or @airframeui/core/classes. get_pattern("app-shell") and get_example("divider") resolve via aliases. Compose valid semantic HTML — follow htmlSemantics (e.g. a a, button div, button[type], img[alt]). Prefer useCases / guidelines on a pattern over guessing look-alikes; treat must / must-not as hard limits; follow useInstead / prefer.

@airframeui/tokens/catalog.json (also import { catalog } from '@airframeui/tokens') lists every --af-* token with a description and default colours per mode. It is regenerated on the tokens build from CSS plus token-meta.json. Prefer unsuffixed names in components (--af-color-background, not --af-color-background--dark).

Prefer @airframeui/mcp over dumping catalogs. Loop: search_patterns → get_pattern → get_example → compose af-* → validate_markup. Dense get_pattern includes useCases / guidelines / criteria when present.

Install -D @airframeui/mcp (same version as @airframeui/core) or run with npx -y @airframeui/mcp@<version>. Restart the airframeui server after saving. Full setup: Airframe MCP.

HostFile
Cursor.cursor/mcp.json (or Cursor Settings → MCP)
Claude Code.mcp.json
Codex.codex/config.toml
VS Code.vscode/mcp.json

Dev dependency (recommended):

{
  "mcpServers": {
    "airframeui": {
      "command": "node",
      "args": ["./node_modules/@airframeui/mcp/dist/server.js"]
    }
  }
}

npx (no install) — pin the version so @latest cannot drift from core:

{
  "mcpServers": {
    "airframeui": {
      "command": "npx",
      "args": ["-y", "@airframeui/mcp@<version>"]
    }
  }
}

Drop a short, version-pinned agent file into your project so coding agents know the Airframe workflow without scraping the whole docs site:

npm install -D @airframeui/build

npx af init --agents

# or target a specific tool:

npx af init --agents --agent cursor # .cursor/rules/airframe.mdc
npx af init --agents --agent claude # .claude/CLAUDE.md

# --force replaces a marked Airframe file only; it will not overwrite a product AGENTS.md

What it writes:

  • AIRFRAME.md by default (--agent agents / codex) — does not replace a product AGENTS.md
  • Cursor / Claude paths as above, or --agent-docs-path <file>
  • A reliable package.json "af" script alias (skip with --no-script)

The file tells agents to: use @airframeui/core/catalog as the class source of truth (same as IntelliSense / ESLint / MCP), follow the design rationale (structure over decoration, one language, compose existing Airframe, progressive density), pick a close blueprint or block before composing patterns and layouts, wrap an existing UI kit rather than duplicating a look, run search_patterns → get_pattern → get_example → validate_markup before inventing classes, and avoid Tailwind/Bootstrap leftovers, hardcoded colours, and recreated patterns. Re-run after upgrading @airframeui/core to refresh the pinned version block.

They are complementary, not two versions of the same thing. Both use @airframeui/core/catalog and @airframeui/core/classes — the same source of truth as IntelliSense and the ESLint plugin.

Agent bootstrap (af init --agents)Airframe MCP (@airframeui/mcp)
What it isA static instruction file in the repoA running stdio server the host talks to
Package@airframeui/build@airframeui/mcp
What it doesWrites AIRFRAME.md / a Cursor rule / Claude file + an "af" npm scriptLets the agent look things up on demand
Source of truthPoints at the catalog; interpolates layout/pattern names from itReads the catalog and token files at runtime
Puts CSS on the pageNoNo

MCP is the toolbox (search_patterns, get_pattern, validate_markup, …). Bootstrap is standing orders so the agent actually follows that loop. You can use MCP with no bootstrap. Bootstrap without MCP still helps (agents can import the catalog) but they lose live search and validate_markup. Connect MCP separately — this command does not write .cursor/mcp.json. See Airframe MCP.

After upgrading Airframe packages, the agent file is a snapshot. Check it, then refresh:

npx af doctor
npx af init --agents

af doctor fails if AIRFRAME.md (or the Cursor/Claude file) is pinned to an older @airframeui/core than you have installed, or if @airframeui/mcp / @airframeui/build do not match core. af init --agents rewrites the marked block from the current catalog (no --force needed if the markers are still there). If the file exists without markers, the command refuses — it will not append into a product AGENTS.md. --force is refused on unmarked files.

Also documented under Build Tool → af init —agents.

  • Semantic HTML first — <button type>, <a href>, <label>, real headings. Follow catalog htmlSemantics (E/W).
  • Token-first theming — prefer --af-color-* / other --af-* tokens; do not invent unknown token names
  • One look — Use af-* as the system, or wrap it in a kit. Do not ship a parallel palette or layout grammar (.ui-stack that copies af-stack)
  • Wrappers sit on af-* — <my-button> / <MyButton> is an af-btn with framework inputs, not a restyled cousin. Keep kit JS for behaviour CSS cannot do (showing toasts, focus trap, toggling af-is-collapsed)
  • Readable DOM — avoid utility soup; prefer patterns + structure classes
  • Do not invent — no unknown af-* / --af-* names, no parallel look, no recreating catalog patterns with utilities
  • af- prefix — only use known Airframe classes (see @airframeui/core/classes)
  • Minimal classes — components include sensible defaults; don’t restate them. Filled af-btn is already primary — do not add af-is-primary except with af-is-ghost
  • Modifiers — variants/state use af-is-* (e.g. af-btn af-is-outline, af-btn af-is-ghost)
  • Responsive — breakpoint suffixes use @ in HTML: af-grid-2@md, af-stack@lg
  • Framework attrs — class in HTML/Vue/Angular/Astro; className in React

If the project has a UI kit, product templates compose af-stack / af-inline / af-grid for layout and <my-*> for widgets. Do not use af-btn in product templates when <my-button> exists. Teams without a kit use af-* directly. Full contract: Use as much as you need.

Runtime CSS variable overrides are the default. When mapping an existing design system into Airframe tokens:

  • Theme Studio — upload Tokens Studio / DTCG / CSS / Figma Variables and export theme.css
  • CLI — npx af theme generate --from ./tokens.json -o theme.css (via @airframeui/build)
  • MCP — generate_theme / lint_theme on @airframeui/mcp
  • Rules — @airframeui/theme/rules and @airframeui/theme/mapping after npm install -D @airframeui/theme

See Figma plugin, Airframe → Figma, Theming, and Theme package.

Canonical patterns (aligned with AIRFRAME_RULES.md):

Cards already display: flex; flex-direction: column with a default gap (--af-space-4). Do not add redundant stack/gap.

<!-- ✅ Good: default card stacking -->
<div class="af-card">
  <h2 class="af-card__title">Title</h2>
  <div class="af-card__body">Content</div>
</div>

<!-- ❌ Avoid: redundant stack/gap on cards -->

<div class="af-card af-stack af-gap-md">
  <h2 class="af-card__title">Title</h2>
  <div class="af-card__body">Content</div>
</div>

af-grid-* includes grid display. Default gap is --af-space-3 (same as af-gap-md).

<!-- 2 equal columns -->
<div class="af-grid af-grid-2 af-gap-lg">
  <div>Item 1</div>
  <div>Item 2</div>
</div>

<!-- Responsive equal columns -->

<div class="af-grid af-grid-1 af-grid-2@md af-grid-3@lg af-gap-lg">
  <div>Item 1</div>
  <div>Item 2</div>
  <div>Item 3</div>
</div>
<div class="af-grid">
  <div class="af-col-span-8@lg">Main content</div>
  <div class="af-col-span-4@lg">Sidebar</div>
</div>

Note: af-grid:has(> [class*='af-col-span']) switches to 12 columns. Default gap is --af-space-3 (af-gap-md). Override with af-gap-sm / af-gap-lg when needed.

<div class="af-stack af-gap-lg">
  <header class="af-inline af-justify-between">
    <h1>Title</h1>
    <div class="af-inline af-gap-sm">
      <button type="button" class="af-btn">Action</button>
      <button type="button" class="af-btn af-is-ghost">More</button>
    </div>
  </header>
</div>
<div class="af-app">
  <aside class="af-app__sidebar">…</aside>
  <main class="af-app__main">
    <nav class="af-nav-pills" aria-label="Settings">
      <a href="#profile" aria-current="page">Profile</a>
      <a href="#security">Security</a>
    </nav>
    <label class="af-action-row">
      <span class="af-action-row__copy">
        <span class="af-action-row__title">Email alerts</span>
        <span class="af-action-row__hint">When someone mentions you</span>
      </span>
      <span class="af-action-row__control">
        <input type="checkbox" role="switch" />
      </span>
    </label>
  </main>
</div>

Do not use af-tabs for settings nav (af-nav-pills) or af-field-inline for action rows (af-action-row). Full-page waits use af-page-loader + af-spinner, not af-backdrop. Overlay af-drawer width is --af-drawer-width, not --af-sidebar-width.

First focusable element in the body, before site nav. tabindex="-1" on the target so Safari moves keyboard focus. Do not also add af-sr-only-focusable. Do not put af-skip inside <nav>.

<a href="#main" class="af-skip">Skip to content</a>
<nav class="af-navbar">…</nav>
<main id="main" tabindex="-1">…</main>
<!-- ✅ Good: semantic element, no class needed -->
<h1>Title</h1>
<p>Body text</p>

<!-- ✅ Good: visual override when semantics differ -->

<h2 class="af-text-h1">Visually H1, semantically H2</h2>
<p class="af-text-caption">Caption</p>

<!-- ❌ Avoid: redundant class on semantic element -->

<h1 class="af-text-h1">Title</h1>

There is no af-is-block, af-is-full, or af-btn-block.

Form controls (af-input, af-textarea, af-select, af-range) are already width: 100%. Do not add af-w-full.

Buttons are inline-flex. Stretch with af-w-full. Do not use af-block on af-btn.

<!-- Form control: already full-width -->
<input type="email" class="af-input" />

<!-- ❌ Avoid: redundant on form controls -->

<input type="email" class="af-input af-w-full" />

<!-- Button: shrink-to-content; compose af-w-full when needed -->

<button type="submit" class="af-btn af-w-full">Continue</button>

<!-- ❌ Avoid: invented modifier -->

<button type="submit" class="af-btn af-is-block">Continue</button>