For a long time I treated our component library like a shared folder: a place to drop buttons and modals so we wouldn't rewrite them. The day several portals in the monorepo started depending on it at the same time, I realized it wasn't a folder anymore. It was a product, with real users —the rest of the team— who feel every careless change. Since then I've maintained it as Tech Lead at JXBS with that mindset: my users are other developers, and their experience matters as much as the end user's.
Tokens are the contract, not a detail
Before you talk about components you have to talk about tokens, because they're the first thing you break without noticing. A token is a design decision with a stable name: a color, a spacing step, a type scale. If one portal hardcodes #2563eb and another uses var(--color-primary), you don't have a design system, you have two systems that happen to match by accident.
I treat tokens as the public contract. Consumers don't ask for hex colors; they ask for semantic intent.
:root {
/* Primitives: never used directly in components */
--blue-600: #2563eb;
--gray-900: #111827;
--space-2: 0.5rem;
--space-4: 1rem;
/* Semantic: this is what the consumer touches */
--color-action: var(--blue-600);
--color-text-strong: var(--gray-900);
--radius-control: 0.5rem;
}
[data-theme="dark"] {
--color-text-strong: #f9fafb;
}
The semantic layer is what lets me theme without touching a single component. I change the mapping, not the signature. And that split between primitives and semantics is exactly what stops someone from coupling their portal to a specific blue that I'll want to move tomorrow.
A component's API is a promise
With React 19 and TypeScript, a component's signature is a contract as serious as any HTTP API. Every prop I expose is something I'll have to hold up across versions. That's why I lean on composition over configuration: instead of a <Card> with twenty boolean props, I expose pieces that combine.
// Bad: configuration that grows without end
<Card title="..." footer withBorder elevated dense hasIcon />
// Better: composition, the consumer assembles their case
<Card>
<Card.Header>...</Card.Header>
<Card.Body>...</Card.Body>
<Card.Footer>...</Card.Footer>
</Card>
Two rules I don't negotiate. First: don't leak the internal DOM. If I expose a className that lands on some arbitrary <div>, and tomorrow that <div> disappears, I break everyone who relied on that structure. I'd rather offer explicit extension points (asChild, slots, typed variants) than let people style my guts. Second: stable props are sacred. Renaming variant to kind "because it reads better" isn't a refactor, it's breaking six teams at once.
💡 A design system isn't measured by how many components it has, but by how many people can build on top of it without messaging you.
Versioning inside the monorepo without hurting people
Turborepo and pnpm let everything live together, and that has a trap: it's far too easy to "bump everything" at once. I use changesets with real semver. A semantic color change is a patch. A new optional prop is a minor. Removing a prop, renaming it, or changing DOM that someone might be selecting is a major —and a major demands notice, a migration guide, and sometimes a window where the old version keeps living alongside the new one.
What I religiously avoid is the reflex "bump everything." If I only touch Button, I don't want to force a portal that doesn't even use it to rebuild and re-verify. Granular versioning is what keeps trust alive: when you upgrade, you know what's going to hurt before it hurts.
Accessibility for free, and the social side
At Acid Tango, in Madrid, I pushed WCAG 2.1 AA as a team standard, and that experience changed how I build libraries. The best accessibility is the kind the consumer gets for free. If my Dialog handles focus, Escape, aria-modal, and returning focus to the trigger, then every portal is accessible without its developer knowing anything about ARIA. Keyboard support, focus management, and contrast aren't optional features: they're the floor.
But the hardest part isn't technical, it's social. A design system has governance whether anyone writes it down or not. I try to make it explicit: who decides what gets in, how you propose a new component, and —the uncomfortable one— how you say no. I say no when something serves a single portal; that lives in the portal, not in the core. To onboard a new component I ask for three things: a real, repeated use case, documentation with copy-pasteable examples, and accessibility solved before it merges. Without examples, people reinvent; and every reinvention is a crack in the contract.
What I'd do differently in 2026
I'd document before coding. For a while documentation showed up afterward, and that's too late: if you can't explain the API in a short example, the API is wrong. Today I'd write the usage example first and let it dictate the signature.
I'd also be more aggressive about graduated deprecation. Marking a prop @deprecated in TypeScript, letting it keep working for a few versions, and warning on every build is infinitely kinder than a surprise major. A mature design system isn't measured by how fast it changes, but by how predictable it is when it does.