DEV Community

Cover image for 🧩 One design system, native to both React and Angular
BPDM 🧩⚡
BPDM 🧩⚡

Posted on • Originally published at ui.bpdm.dev

🧩 One design system, native to both React and Angular

We run a React app and an Angular admin panel at work. Same company, same brand, and on paper the same design.

On screen it was a different story. The React button had a 6px radius; the Angular one had 4px. The focus rings were two slightly different blues. Nobody noticed until somebody did. And every time design changed a token, someone got to hand-port it into two codebases. Twice the work, and it still drifted.

So I went looking for something that treated both frameworks as equals. The React kits don't speak Angular. The Angular ones don't share a look with anything on the React side. Nothing let me define the design once and have it show up, the same, in both. So I built bpdm/ui.

One rule: the look lives in tokens

I gave myself one hard rule: nothing about how a component looks is allowed to live inside the React or Angular code. Colour, spacing, radius, the easing on transitions, all of it sits in @bpdm/tokens as plain CSS variables, and both framework packages just read from there. The component owns structure, behaviour, and the accessibility plumbing. The look comes from the tokens.

@import "tailwindcss";
@import "@bpdm/tokens/tokens.css";
Enter fullscreen mode Exit fullscreen mode

Change one token and both frameworks move together. There's no "now go sync the Angular theme" step, because there's only one theme to sync. Four ship in the box (two light, two dark). Override the variables and you've re-skinned all of it.

The same component, twice

React:

import { Button, Badge } from "@bpdm/ui";

export function Example() {
  return (
    <Button variant="primary">
      Get started <Badge appearance="soft">New</Badge>
    </Button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Angular:

import { Component } from "@angular/core";
import { BpdmButton } from "@bpdm/ng";

@Component({
  selector: "app-root",
  imports: [BpdmButton],
  template: `<button bpdmButton>Get started</button>`,
})
export class App {}
Enter fullscreen mode Exit fullscreen mode

Same padding, same radius, same focus ring. The accessibility isn't literally shared code: Radix does that work on the React side, the Angular CDK on the Angular side. But both follow the same ARIA patterns, so keyboard and screen-reader behaviour ends up matching instead of being reinvented twice.

Where it's at

38 components, both frameworks. I started with the stuff dashboards and back-office tools actually lean on, the pieces I kept rebuilding by hand: money inputs, masked and secure fields, KPI stat cards, status timelines, multi-selects, tree-selects. It's all typed and tree-shakeable, MIT-licensed.

I'll be straight about what it isn't: it's young. It hasn't survived years in production yet, and the Angular side hasn't taken the same beating the React one has. If you try it and something feels even slightly off between the two frameworks, that's the bug I want to hear about first. Parity is the whole reason this exists, so a gap there means I got the one thing wrong.

A few things I traded away on purpose

  • It needs Tailwind v4. Not using Tailwind? Then it's not your library, and that's fine.
  • One shared token set means less room to go off-script in a single framework. That was the trade I wanted: consistency over local freedom.
  • It's a solo project. If you're weighing it for something serious, that's a real risk, and I'd rather say so than hide it.

Have a look

If you work across React and Angular, I'd really like to know whether the parity holds up in a real codebase and not just in my demos. That's the thing I most want to find out.

Top comments (0)