Skip to content

Airframe and shadcn

shadcn solved a real problem: you should own the component source. Copy the file into the app. Change it. It is yours. No node_modules black box for the button.

That is not the same problem Airframe solves.

Airframe asks: what stays stable when the framework and the widget library change? Tokens, layout primitives, and a readable class contract. The grammar under the kit. Not the React file you copied.

They can sit together. They should not be confused.

shadcnAirframe
OwnsComponent source in your repoStructure: tokens, layouts, CSS patterns
Default engineReact (CLI copies TSX)Any engine. The CSS does not care
RuntimeWhatever you copied (often Radix / Base UI / React Aria)None. Native HTML for behaviour
CustomisationEdit the fileOverride --af-*. Wrap af-btn
Failure modeEvery product forks a different buttonTwo looks: kit CSS beside af-*

shadcn is a distribution model for React components. Airframe is a structural UI system. One is how you take delivery of widgets. The other is the language those widgets (or Angular ones, or plain HTML) are built in.

Use a shadcn-style workflow when:

  • The team is React-first and wants to edit dialogs, popovers, and menus as files.
  • You need behaviour that native HTML does not cover, and you want that JS in-tree.
  • You already chose primitives (Base UI, React Aria, Radix) and want to swap the foundation without losing the copy-paste habit.

That is a strong product. It still does not give Angular, Vue, or an AI agent a shared visual contract by itself. Each new engine is another copy of the idea.

Use Airframe when:

  • More than one engine must look like the same product.
  • You want page layout (af-stack, af-inline, af-grid) that reads as intent, not utility soup.
  • Tokens should live in CSS and work at runtime without a rebuild.
  • Agents should look up coded rules instead of inventing class names.
  • The kit already exists (<my-button>, @acme/ui) and you need it to sit on one look.

Airframe does not replace “copy the component into components/ui.” It is what that component (or its Angular twin) should be made of.

React kit in the shadcn shape. Airframe as the look.

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

export function Button({
className,
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return <button className={clsx('af-btn', className)} {...props} />;
}

You still own the file. Variants map to af-is-*. Layout on the page is af-stack, not a one-off flex flex-col gap-4 next to the kit. Tokens are --af-*, not a second colour file.

If Base UI or React Aria handles the dialog, keep that primitive. Style the surface with Airframe tokens and patterns. The JS layer is replaceable. The look is not a parallel system.

Same idea as Use as much as you need. Airframe is the grammar. A kit is the typed API on top, if you have one. shadcn is one way to deliver that kit in React.

  • Tailwind composes appearance as utilities. Airframe names structure. Comparison: Airframe vs Bootstrap vs Tailwind.
  • Base UI / React Aria / Zag own accessible behaviour. Put them under widgets. Do not ask them to be the design system.
  • Web Components own a framework-independent implementation. Still a widget layer. Sit them on the same tokens if you use them.

The question is not “which library has more buttons.” It is “what is invariant when we change engines.”