How to keep one design system across React and Angular
Two products. Two engines. One brand.
That is the usual setup, not an edge case. Marketing is React. The internal app is Angular. Next year someone generates screens with an agent. The design system should not be rewritten each time.
The failure is tying the look to the engine.
What has to stay the same
Section titled “What has to stay the same”Not the TypeScript. Not the DI. Not the router.
| Shared | Per engine |
|---|---|
| Colour, space, type, radius | Component APIs (<Button> vs <ui-button>) |
| Layout intent (stack, inline, grid) | How you bind events and forms |
| Button / card / input look | Toast services, modal traps, focus JS |
| Focus rings, contrast, reduced motion defaults | Framework a11y primitives if you need them |
If those shared rows live in React components, Angular has to fake them. If they live in CSS, both engines can consume the same file.
Three ways this usually goes wrong
Section titled “Three ways this usually goes wrong”1. Copy the values. A Figma screenshot becomes hex in a React theme and again in an Angular theme. They drift in a week.
2. Pick a winner.
“We will rewrite Angular in React so we can share @acme/ui.” The design system did not need that migration. The component runtime did.
3. Buy a three-framework widget kit. One Dialog implementation, three bindings, same keyboard map. That solves behaviour portability. It does not give you page layout, tokens, or a contract agents can follow. It also locks you to that kit’s component model. Different job. See Framework-agnostic UI is not a design system.
Put the contract in CSS
Section titled “Put the contract in CSS”One import. Same classes. The engines only differ in how they bind.
React
export function TeamHeader() {
return (
<header className="af-inline af-justify-between">
<h1>Team</h1>
<button className="af-btn">Invite</button>
</header>
);
}Angular
@Component({
selector: 'app-team-header',
standalone: true,
template: `
<header class="af-inline af-justify-between">
<h1>Team</h1>
<button class="af-btn">Invite</button>
</header>
`,
})
export class TeamHeader {}Same --af-* tokens. Same af-stack / af-btn grammar. No shared runtime. That is the point: class vs className is the whole framework gap for look and layout.
Install once:
npm install @airframeui/core@import "@airframeui/core/core.css";React: import the CSS from the bundle entry. Angular: add it to styles in angular.json, or @import it in styles.scss. Guides: Framework support, Angular.
Keep typed APIs on the engine
Section titled “Keep typed APIs on the engine”Product templates should not be a soup of af-btn in one app and <ui-button> in the other unless that is deliberate.
If you already have a kit, wrap the CSS. Do not restyle a cousin.
Angular wrapper
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');
}React wrapper
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}
/>
);
}React app uses <Button>. Angular app uses <ui-button>. Both paint af-btn. Copy-paste React versions: React Recipes. Contract: Use as much as you need.
You can also skip wrappers and use af-* directly in templates. Valid. Do not run wrappers and a second .ui-* look.
What CSS cannot share
Section titled “What CSS cannot share”Native <button>, <dialog>, and <details> go a long way. Toasts, focus traps, comboboxes, and rich menus need JS.
That JS should stay in the kit (or in React Aria / Angular CDK). Do not wait for a CSS system to invent a keyboard map. Do not duplicate a Dialog primitive in both engines and give them different padding and colour.
Look from Airframe. Behaviour from the engine’s primitive, or from native HTML. Separate design from behaviour.
Tokens: one file, both apps
Section titled “Tokens: one file, both apps”Map brand once. Alias existing --color-* into --af-*, or invert after the spike.
:root {
--af-base-primary: #0e80eb;
--af-font-body: 'Inter', system-ui, sans-serif;
}Ship that file in both builds. Dark, light, and high-contrast are already in the token layer. Named products use [data-brand]. Theming.
If the brand already lives in Tokens Studio or Figma, map into --af-* with Theme Studio. Generating Figma Variables from Airframe is on the roadmap. Until then, git CSS is the source both apps consume. Design tokens as the source of truth.
Agents
Section titled “Agents”A third engine shows up whether you planned it or not. If React has Tailwind soup and Angular has BEM, the model invents a third look.
Give it one grammar: af-stack, af-btn, --af-*, and the published rules (@airframeui/core/rules, MCP). Same contract in both repos. AI Rules.
A rollout that does not require a rewrite
Section titled “A rollout that does not require a rewrite”- Import
@airframeui/core/core.cssin both apps (or in each kit package). - Map tokens. Stop adding new colours in feature CSS.
- Convert one shared screen (login, settings, dashboard header) to
af-stack/af-inline/af-grid. - Point one button in each kit at
af-btn. Leave the public API stable. - Repeat. Delete parallel palettes as you go.
You do not need every pattern. Tokens only is a valid stop. Use as much as you need.
What this is not
Section titled “What this is not”Airframe is not @airframeui/react plus @airframeui/angular. There are no official runtime packages. That is intentional. The design system is the CSS contract. The engines stay yours.
If you needed one JS Dialog with identical behaviour in React and Angular, that is a different layer. Sit it on the same tokens. Do not replace the grammar with it.
Next steps
Section titled “Next steps”- One UI language. Everywhere. — the same idea across more than two engines.
- Framework-agnostic UI is not a design system — widget kits vs a CSS contract.
- Angular and React Recipes — install and wrap.
- Use as much as you need — tokens, layouts, patterns, or wrappers.