I have built reusable components the wrong way more than once. Early on I thought a shared library just meant putting a Button in a folder everyone could import. Then you get five teams, two frameworks, a rebrand, and suddenly that innocent little Button has nine variants and no one knows which one is the real one. That is when you learn that a component library at enterprise scale is infrastructure, not a convenience. And like all infrastructure, it is boring right up until it breaks.
The moment it clicked for me was mid-migration on an enterprise platform I was leading. We were moving core modules from Angular to React while other parts of the same app were still being lifted to Angular 18, all at once. Two frameworks, one product, and several teams touching the same UI surface. A Button that looked perfectly fine in isolation started rendering with identical padding and hover states depending on which framework it landed in. By the time we traced it back, three different teams had already built their own patched version to work around it. That was the moment shared tokens stopped being a nice-to-have and became the fix.
So this is the article I wish someone had given me. Not “how to extract a reusable component,” there are plenty of good tutorials on that. This is the hardest part: keeping a shared library alive across many teams, across both Angular and React, inside an organization big enough that a careless change ripples into dozens of applications before lunch. I have spent the last few years doing exactly this, first modernizing enterprise libraries apps to Angular 18 and later standardizing React and TypeScript component libraries across teams. Most of what follows I learned the slow way.
First, An Unpopular Opinion: You Might Not Need One
Reusable libraries have a cost, and people forget that. Every abstraction is a thing someone has to learn, document, version, and maintain forever. If you have one app and one team, a shared library is often premature. You will spend more time building the abstraction than you ever save.
My rough rule of thumb, and it is rough, is that a real shared library starts paying off when you cross three lines at once. More than a couple of teams shipping UI, and more than a couple of apps that are meant to look like the same product. And a business that changes its mind about branding or compliance often enough that consistency is a recurring pain, not a one-time cleanup.
If you only cross one of those lines, keep it simple. For small teams, a bit of copy and paste is underrated, and I say that knowing it is not a fashionable opinion.
The Layers That Keep It Maintainable
When the library is justified, layering decides whether it survives. A flat pile of components rots, and a layered one bends.
Bottom to top, the way I think about it. Design tokens sit at the very bottom, your raw values (colors, spacing, font sizes, radii) as plain data with no framework attached. This is the single source of truth, and it is deliberately dumb.
json
{
“colorPrimary”: “#0033A0”,
“colorPrimaryHover”: “#00287D”,
“space2”: “8px”,
“radiusSm”: “4px”
}
Above that sit the primitives, your atoms: Button, Input, Icon, Text. They are small, boring, and ruthlessly generic. They read from the tokens and have no idea what app they are living in. Then come the composed components, things like a FormField that wraps a label, an input, and an error message. And past that you get into app level patterns, which usually should not live in the shared library at all. That last boundary is the one everyone gets wrong. Not everything reusable inside your app belongs to everyone.
Here is the same primitive in both frameworks, reading the same token, because that is the whole point.
react:
jsx
import { tokens } from “@company/tokens”;
export function Button({ variant = “primary”, …props }) {
return (
);
}
Angular (the tokens are resolved in getters so the input value is actually available when they are read, which is the bug most people hit if they assign in the field initializer):
typescript
import { Component, Input } from ‘@angular/core’;
import { tokens } from ‘@company/tokens’;
@Component({
selector: ‘ui-button’,
template: `
`,
})
export class ButtonComponent {
@Input() variant: ‘primary’ | ‘secondary’ = ‘primary’;
get background(): string {
return this. variant === ‘primary’ ? tokens.colorPrimary: tokens. colorPrimaryHover;
}
get radius(): string {
return tokens . radiusSm;
}
get padding(): string {
return ${tokens . space2} 16px;
}
}
Two implementations, one truth underneath. When the brand blue changes, it changes in one file and both frameworks pick it up. That is worth a lot when you are the one answering for why the new blue only landed in half the apps.
The Cross Framework Problem Nobody Warns You About
Here is what most guides quietly avoid, probably because it is annoying. Big organizations rarely run on one framework. You inherit an Angular app from a few years back and a newer React app someone spun up last year, and now you are meant to make them feel like one product. Welcome to the current job.
Three ways I have seen this handled, none of them free. Duplicate and sync, you keep an Angular library and a React library side by side, sharing only tokens. Simplest to start, easiest to let drift. Six months in, the React Button has a loading state the Angular one does not, and you are back to inconsistency. Works, barely, with real discipline and a small surface.
Web Components as the shared primitive layer. Build the low level pieces once as custom elements, wrap them thinly per framework. Lovely in theory, but in practice you fight form integration, event handling, and styling quirks inside the shadow DOM, and both Angular and React have opinions about wrapping custom elements. I like this approach more every year, but go in with your eyes open.
Shared core with thin wrappers, the one I read toward now. Keep tokens plus framework free logic (validation, formatting, state machines) in a plain TypeScript package, and let each framework provide only the rendering layer. Most bugs live in logic, not markup, so sharing the logic is where the payoff is.
Whatever you pick, pick it on purpose and write down why. The failure mode is not choosing wrong. It is choosing by accident and finding out two years later.
Versioning, Or How Not To Break Forty Teams Before Lunch
This is what separates an enterprise library from a side project. In a small app a breaking change is a quick fix, and in a large org it is an incident.
I learned this the hard way during that same migration, standardizing a component library across Angular, React, TypeScript, and Angular Material. A prop rename on a shared form control felt completely trivial on my end. It silently broke validation in a module another team owned, because they were passing the old prop name straight through. Nothing failed loudly. Validation just quietly stopped happening, which is about the worst way for something to break. After that, we moved to a strict deprecation window on anything published, with console warnings well before removal, and I started writing changelogs as if the reader had five minutes and zero context on why I did any of it.
So, treat the library like the public API it now is. Semantic versioning is not optional, and a prop rename is a breaking change even when it feels tiny to you, because somewhere a team you have never met is passing that prop. Never delete, deprecate first: keep the old thing working, mark it clearly, warn in the console, and give people a version or two to migrate. Ripping things out feels clean and will make you very unpopular. Have a real contribution model too, decide who can add to the library and who can only request, because if everyone can push anything it becomes a junk drawer within a quarter, and if only one team can, you become the bottleneck everyone routes around.
Documentation Is The Whole Product
An undocumented component library is a dead one, and I mean that. If a developer on another team cannot find your component and figure out how to use it in about a minute, they will build their own, and now you have the exact fragmentation the library was meant to prevent.
Storybook is the standard for a reason. Living documentation, every component with its variants and states, that cannot go stale because it renders the real component. Pair it with a clear “use this when, do not use it when.” The “do not” is what people skip and it is the most useful part.
Bake accessibility at the primitive level. Get ARIA roles, focus handling, and keyboard behavior right once, in the Button and the Input, and every team downstream inherits it. Push accessibility up to app teams and it will not happen consistently, because it never does. WCAG compliance is far cheaper when it lives at the bottom of the stack.
Testing So You Can Sleep
The scary thing about a shared library is that a bug doesn’t hit one screen, it hits everything. So the bar is higher than for a normal app.
Three layers I rely on. Unit tests for behavior, Jasmine and Karma on the Angular side, Jest and Testing Library on the React side. Visual regression tests, because a component can pass every unit test and still look broken, and a screenshot diff catches what assertions miss. And automated accessibility checks in CI, so a missing label fails the build instead of a user.
A tiny baseline, nothing fancy:
javascript
import { render, screen, fireEvent } from ‘@testing-library/react’;
import { Button } from ‘ . /Button’;
test(‘button renders its label and fires onClick’, () => {
const onClick = jest . fn();
render();
fireEvent . click(screen . getByText(‘Save’));
expect(onClick) . toHaveBeenCalledTimes(1);
});
Not glamorous, neither is a smoke detector. You still want one.
How Do You Know It Worked?
Do not measure success by how clever the library is. Measure it by what the teams around you can do that they couldn’t before.
For me the clearest number was feature development time. Once the shared patterns were actually adopted, it dropped by close to 20%, simply because teams stopped rebuilding the same form and the same table over and over. The related migration and API work around it moved other numbers too, roughly a 30% cut in query response times and over 30% faster page loads after the performance work that came with it. But honestly, the signal I trusted most was not on a dashboard. It was watching teams who used to quietly route around the library start reaching for it by default, and seeing far fewer one off components getting built from scratch. That is the point where a shared library stops being something you enforce and becomes something people actually want.
Conclusion
A reusable component library at enterprise scale is less about React or Angular tricks and more about treating your UI like a product whose users happen to be other engineers. Layer it so it can bend. Share the tokens and the logic, not just the markup. Version it like the public API it is. Document it like your job depends on adoption, because it kind of does. Test it like a bug ship everywhere at once, because it does.
I am still learning parts of this. The cross framework story keeps getting better as Web Components mature, and I suspect the “Angular library plus React library” split will feel old-fashioned in a few years. For now, this is what you have held up for me in the real world, mess and all.
One thing to take away: the hard part was never building the component. It was keeping forty teams agreeing on what a Button is. Solve that and you have solved the current problem.
Vaishnavi Khosla is a software developer focused on full stack development, enterprise security, and front end architecture, currently building secure identity and access management platforms in financial services.




