Skip to content

Use as much as you need

One UI language. Everywhere. Airframe is a structural UI design system for humans and AI. Your framework provides the engine. Airframe provides the structure.

Use af-* directly in templates. That is Airframe as the design system. If you already have a kit, wrap those patterns in <my-button> / <MyButton>. Both are valid. Do not run two looks.

When there is a kit, it sits on Airframe, not beside it. If .ui-* classes keep painting their own buttons, cards, and spacing, you get two systems fighting in every template.

LayerOwnsDoes not own
Airframe (.af-*)Reset, tokens, layout (af-stack / af-inline / af-grid), look of button / card / inputDomain UI, DI, routing, JS widgets
Your UI kit (<my-*>, .ui-*)Framework wrappers, toast / modal / sidebar, product chromeA parallel palette or layout grammar
Product / appsCompose af-* layouts + kit widgetsAd-hoc page BEM that copies Airframe

Airframe does not require a UI package. Teams without one use af-* directly in templates. Teams that already have @acme/ui (or similar) should treat that package as the only place @airframeui/core is imported. Same idea as keeping vendor SDKs in a platform layer.

Pick a depth. Stay there until you need more.

DepthYou take from AirframeYou keep
Tokens--af-* mapped to your brandExisting components and layout
Layoutsaf-stack, af-inline, af-grid, and friendsYour buttons, cards, overlays
PatternsAll 57 CSS patterns (af-btn, af-card, af-input, …) as classesFramework behaviour you already own
WrappersPatterns inside <my-button>, <my-card>, …Typed inputs, slots, services

You can mix depths on purpose. Layouts from Airframe plus a custom toast service is normal. Two palettes and two layout grammars is not.

A kit button is an af-btn with framework inputs. Not a restyled cousin.

Angular

import { Component, input } from '@angular/core';

@Component({
selector: 'ui-button',
standalone: true,
template: `
    <button
        class="af-btn"
        [class.af-is-primary]="variant() === 'primary'"
        [class.af-is-outline]="variant() === 'outline'"
      >
      <ng-content />
    </button>
  `,
})
export class UiButton {
readonly variant = input<'primary' | 'outline' | 'secondary'>('primary');
}

Host / focus / slot glue can stay in .ui-button. Keep real .ui-* CSS for things Airframe cannot do: ToastService, modal focus trap, sidebar collapse, marketing chrome.

React

import { clsx } from 'clsx';
import type { ButtonHTMLAttributes } from 'react';

export function Button({
variant = 'primary',
className,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'outline' | 'secondary';
}) {
return (
  <button
    className={clsx(
      'af-btn',
      variant === 'primary' && 'af-is-primary',
      variant === 'outline' && 'af-is-outline',
      className,
    )}
    {...props}
  />
);
}

Copy-paste React wrappers: React Recipes.

Same rule for card, input, badge. If the kit component exists, product templates use the kit. If it does not, use the af-* pattern until you wrap it.

Layout classes from Airframe. Interactive pieces from your kit.

<main class="af-stack af-gap-lg">
  <header class="af-inline af-justify-between">
    <h1>Team</h1>
    <my-button>Invite</my-button>
  </header>
  <my-card>…</my-card>
</main>

Kill page-local .page-container / .auth__card grids as you touch them. Do not invent a third layout language.

Teams without a kit use af-btn and af-card in the same slots. The page structure does not change.

Map once. Then stop duplicating.

:root {
  --af-base-primary: var(--color-primary);
  /* or invert: --color-* aliases --af-* after the spike */
}

Pick one canonical set. After adoption, --af-* should be canonical. Keep --color-* (or --bs-*, --mdc-*) as aliases until old SCSS dies.

Two token files forever is how the dual-system leak starts. Full override workflow: Theming. Mapping an existing token file: Theme Studio. Figma as a generated view: Design tokens as the source of truth.

The payoff is a contract coding agents can follow. Add this next to npx af init --agents (or in your own AGENTS.md):

  • Layoutaf-stack / af-inline / af-grid from the catalog. Do not invent layout utilities.
  • Widget<my-*> from your kit when a wrapper exists.
  • Never af-btn in product templates if <my-button> exists.
  • Never new .ui-* layout utilities that copy Airframe.
  • Never a second palette. Alias into --af-*.
  • Do keep kit-only CSS for behaviour Airframe cannot do (toast, modal trap, sidebar).

Authoritative markup rules: AI Rules and @airframeui/core/rules.

  • Do not keep restyling .ui-button / .ui-card as the brand. Put the look on af-btn / af-card and wrap.
  • Do not let product mix Tailwind-style soup with both prefixes.
  • Do not rewrite the whole app before you know the split works. Spike one surface (auth login or dashboard overview), then roll.
  • Do not import @airframeui/* from every feature folder if you have a platform UI package. Import it once, in the kit.
  1. Import @airframeui/core/core.css in the app (or in the kit, if you have one).
  2. Map tokens. One alias file. No new colours in feature SCSS.
  3. Replace one page’s layout with af-stack / af-inline / af-grid.
  4. Use af-btn / af-card in templates, or point one kit component at them. Leave any public API stable.
  5. Repeat. Delete parallel CSS as you go.

Airframe is the structure. Use it as the design system, or put a typed kit on top.