Skip to content

Airframe vs shadcn/ui

shadcn/ui and Airframe get compared a lot. They solve different problems. Many teams will want both.

shadcn/ui gives you component source. A CLI copies React files into your repo. You own them and edit them.

Airframe gives you a UI language. Design tokens, structure primitives, and accessible CSS patterns. One CSS import. Any framework.

Short version: shadcn/ui owns how components behave. Airframe owns how the interface looks and is arranged.

shadcn/uiAirframe
What you getComponent source files in your repoTokens, structure primitives, CSS patterns
EngineReact (community ports exist for other frameworks)Any. HTML, React, Angular, Vue, server templates
StylingTailwind utilities inside each componentNamed af-* classes and --af-* design tokens
BehaviourHeadless primitives (Radix UI, Base UI) handle focus and keyboardNative HTML first (<dialog>, <details>, form controls)
RuntimeWhatever the copied components importNone. CSS only
CustomisationEdit the fileOverride --af-*. Wrap af-* in your own components
LayoutTailwind utilities (flex flex-col gap-4)Structure primitives (af-stack, af-inline, af-grid)
UpdatesYou own the copy. Pull upstream changes by handnpm update. Classes and tokens stay stable
For AI agentsRegistry and MCP server for adding componentsRules, class reference, llms.txt, MCP server, markup validation
  • You build in React and want working components today.
  • You need behaviour-heavy widgets. Combobox, command menu, date picker, dropdown menu.
  • You want the source in your repo, not behind a package version.
  • Your team already writes Tailwind.

shadcn/ui got something important right. You should own your components. Copy the file. Change it. It is yours.

  • You want no runtime. Patterns are CSS on native HTML.
  • You want markup that reads as structure. af-stack instead of flex flex-col gap-4.
  • You want one set of design tokens across products, Figma, and code.
  • You want AI agents to produce consistent UI. Airframe gives them a small, named vocabulary they can look up and validate.
  • You ship more than one framework, or expect to change framework later.

A settings form row with actions.

<!-- shadcn/ui style: Tailwind utilities describe the implementation -->
<div class="flex flex-col gap-4 rounded-lg border bg-card p-6">
  <label class="text-sm font-medium" for="name">Name</label>
  <input id="name" type="text" class="h-9 rounded-md border px-3" />
  <div class="flex justify-end gap-2">
    <button class="h-9 rounded-md border px-4">Cancel</button>
    <button class="h-9 rounded-md bg-primary px-4 text-primary-foreground">Save</button>
  </div>
</div>

<!-- Airframe: classes describe the intent -->
<div class="af-card af-stack af-gap-md">
  <div class="af-field">
    <label class="af-field__label" for="name">Name</label>
    <input id="name" type="text" class="af-field__control af-input" />
  </div>
  <div class="af-inline af-gap-sm af-justify-end">
    <button type="button" class="af-btn af-is-outline">Cancel</button>
    <button type="submit" class="af-btn">Save</button>
  </div>
</div>

The Airframe version is shorter. It is also easier for a person or a model to check. Every class is in the class reference. Unknown names fail ESLint and validate_markup.

The two tools own different layers.

LayerOwner
Design tokensAirframe (--af-*)
LayoutAirframe structure
Visual patternAirframe CSS
Behaviourshadcn/ui and its primitives
Your appYou

Airframe owns how it looks. Your kit owns how it behaves. shadcn/ui is a good kit.

Keep the shadcn/ui ownership model. Keep Radix or Base UI for focus, keyboard, and positioning. Swap the Tailwind styling inside each file for af-* classes. Now every component reads from one token layer and one class vocabulary.

1. Point shadcn/ui variables at Airframe tokens

Section titled “1. Point shadcn/ui variables at Airframe tokens”

shadcn/ui themes through CSS variables. Alias them to --af-* once. Airframe owns the palette. Light, dark, and high contrast follow automatically.

:root {
  --background: var(--af-color-background);
  --foreground: var(--af-color-text);
  --primary: var(--af-color-primary);
  --muted-foreground: var(--af-color-text-muted);
  --border: var(--af-color-border);
  --radius: var(--af-radius-md);
}

Components you have not converted yet now match the ones you have.

Keep the file and its API. Map variants to Airframe modifiers.

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

const variants = {
  default: '',
  secondary: 'af-is-secondary',
  outline: 'af-is-outline',
  ghost: 'af-is-ghost',
  destructive: 'af-is-danger',
  link: 'af-is-link',
};

const sizes = { default: '', sm: 'af-is-sm', lg: 'af-is-lg' };

type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: keyof typeof variants;
  size?: keyof typeof sizes;
};

export function Button({ variant = 'default', size = 'default', className, ...props }: Props) {
  return (
    <button className={clsx('af-btn', variants[variant], sizes[size], className)} {...props} />
  );
}

Call sites do not change. <Button variant="outline"> still works.

Arrange pages with af-stack, af-inline, and af-grid. Keep Tailwind out of layout and colour. Two grammars painting the same element drift apart.

  • AI agents write a lot of your UI: Airframe as the vocabulary. Agents look up patterns and validate markup against it.
  • React only, need rich widgets now: shadcn/ui. Add Airframe tokens when you want one palette across products.
  • Several frameworks, or server-rendered HTML: Airframe. Wrap af-* in whatever components each app needs.
  • A shadcn/ui app that needs to scale into a design system: Both. Keep shadcn/ui for behaviour. Move tokens, layout, and visuals to Airframe one component at a time.