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";
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>
);
}
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 {}
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
- Live components and themes: https://ui.bpdm.dev
- Docs (install, theming, per-component API): https://docs.ui.bpdm.dev
- Source, both libraries: https://github.com/bpdm-hq/bpdm-ui
- On npm:
@bpdm/ui(React),@bpdm/ng(Angular)
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)