Separate design from behaviour
Design systems get stuck when they own too many layers at once.
They pick React. Then they pick a primitive library. Then the visual language is whatever those primitives look like after a theme pass. Swap the engine, or swap Radix for Base UI, and the “design system” has to be rebuilt.
The useful split is:
- Design — tokens, layout, pattern look, focus rings, contrast.
- Behaviour — keyboard map, ARIA widgets CSS cannot express, focus trap, typeahead.
- Engine — React, Angular, Vue, HTML.
Airframe is the design layer. CSS-only. Zero runtime. Your framework provides the engine. A primitive library, or native HTML, provides behaviour.
Native HTML is the default behaviour layer
Section titled “Native HTML is the default behaviour layer”Buttons, links, inputs, checkboxes, <dialog>, <details>. Use them. Airframe styles them.
<button type="button" class="af-btn" onclick="document.getElementById('confirm').showModal()">Save</button>
<dialog class="af-dialog" id="confirm">
<form method="dialog">
<div class="af-dialog-header">
<h2 class="af-dialog-title">Discard draft?</h2>
</div>
<div class="af-dialog-footer">
<button class="af-btn">Close</button>
</div>
</form>
</dialog>You still call showModal(). That is a few lines in the app or kit. It is not a reason to adopt a full widget runtime.
When native is enough, stop. Do not wrap a checkbox in a primitive library “for consistency.”
When you need a primitive library
Section titled “When you need a primitive library”Menus with arrow keys. Combobox. Date picker. Tabs that are not links. A drawer that traps focus.
Those are behaviour problems. Good tools already exist:
| Engine | Behaviour you might sit on Airframe |
|---|---|
| React | React Aria, Base UI, Zag, Radix |
| Angular | CDK, your own FocusTrap, overlay services |
| Vue | Headless UI, Ark, Reka |
| Any | Native <dialog>, plus a small kit helper |
Airframe does not replace those. It should not grow an @airframeui/react-aria either. The primitive stays swappable. The look does not.
shadcn moving toward interchangeable Base UI / React Aria foundations is the same split: do not make the design system own the primitive layer. Airframe and shadcn.
What composition looks like
Section titled “What composition looks like”The kit owns the widget. The widget renders Airframe classes. The primitive owns state and keyboard.
React (shape, not a required dependency)
// Primitive handles open / focus / ARIA.
// Airframe handles the surface.
export function Dialog({ children, ...props }) {
return (
<Primitive.Root {...props}>
<Primitive.Backdrop className="af-backdrop" />
<Primitive.Panel className="af-dialog">{children}</Primitive.Panel>
</Primitive.Root>
);
}Angular
The overlay service stays yours. The panel is still af-dialog. Tokens are still --af-*. Product templates never see a second palette.
Page layout stays Airframe either way: af-stack, af-inline, af-grid. Do not let the primitive library introduce a layout language.
Two failure modes
Section titled “Two failure modes”CSS pretends to be JS.
A “menu” class with no keyboard support, advertised as accessible. Airframe documents pattern contracts and native behaviour. It does not claim a shared keyboard map across frameworks. That claim belongs to widget kits. Framework-agnostic UI is not a design system.
JS pretends to be the design system.
Every product looks like the primitive’s demo until someone writes another theme. Tokens leak into style={{}}. Spacing is magic numbers. Angular cannot come along for the ride.
If you already have the primitive, keep it. Map paint to --af-*. Wrap the trigger in af-btn. Do not restyle a cousin.
Accessibility
Section titled “Accessibility”Airframe builds in what CSS can own: visible focus, reduced motion, contrast-minded tokens, high-contrast themes, RTL-ready layout, semantic HTML defaults.
What CSS cannot own: focus trap, aria-controls wiring, typeahead. Your content and your JS still matter. Aim for WCAG 2.2 AA. Do not treat the CSS package as a compliance certificate.
Pattern-level notes: Accessibility, Pattern contracts.
Use as much as you need
Section titled “Use as much as you need”Tokens only: map --af-*, keep your current Dialog.
Layouts: structure pages, keep your widgets.
Patterns: af-btn / af-card as the look inside those widgets.
Optional a11y JS (focus trap and similar) is later, as a layer on Airframe, not a rewrite of core. Roadmap.
Next steps
Section titled “Next steps”- Use as much as you need — wrap
af-*if a kit exists. - How to keep one design system across React and Angular — same look, native APIs per engine.
- Dialog pattern — CSS surface. You open it.
- Airframe and shadcn — own the file. Do not own a second grammar.