A component library is an API
A shared React component library fails the way public APIs fail. Treating it as one, with a small surface and real versioning, is most of the work.
I am defining the front-end architecture for a product built by a team of eleven, and a large part of that is a shared React component library. The visual side of a design system gets most of the attention. The part that decides whether it survives is less visible: the library is an API, its consumers are your colleagues, and every prop is a promise.
Every prop is forever
The first version of any shared component is too flexible. Someone needs the button a little different, so it grows a prop, and then another:
Each boolean doubles the number of states. Six of them is sixty-four buttons, of which the designers drew four. The other sixty exist, will be used, and will be reported as bugs.
What I try to do now:
Variants, not flags.
variant="primary"andsize="small"are closed lists. A combination that is not in the list cannot be written.No style escape hatch by default. If a component accepts
classNameorstyle, the library's appearance is whatever the last caller made it. When someone needs an exception, that is a conversation about the design, and it should happen in the open.Composition for content, props for behaviour.
childrenhandles the infinite cases. Props handle the few that the component must know about.
The test for a new prop: can I name the second caller? If not, the first caller can wrap the component locally, and the library stays small.
Version it like you mean it
A library used by one application is tempting to treat as a folder. The moment a second consumer exists, a change to a component is a release, and people need to know what kind.
Semantic versioning works here if you are honest about what breaking means. A changed prop name is breaking. So is a changed default, a changed DOM structure that someone's test or stylesheet relied on, and a changed pixel height that pushes a layout. The last two are the ones that cause the arguments.
Two practices made releases calm:
Deprecate, then remove. The old prop keeps working for a release and warns in development. Nobody is forced to stop what they are doing because the library moved.
Ship the migration. For anything mechanical (a renamed prop, a moved import) a codemod does the change across the application. A breaking change that arrives with its own fix is barely breaking.
Documentation that cannot drift
Written documentation of components is out of date the day after it is written. What works is documentation that is executed: every component has stories showing each variant, and those stories are the same code that the visual review looks at. If a state is not in a story, it is not supported, and that sentence settles a lot of discussions.
It also changes who can review. A designer can look at the rendered states and say that one is wrong, without reading a diff.
Hooks change the shape of the question
Hooks landed in React two months ago, and they move the line between what a library should give you as a component and what it should give you as behaviour. A useDisclosure or usePagination hook shares the logic and leaves the markup to the caller. For the cases where teams keep asking for one more prop to tweak rendering, that is a better answer than another prop.
I have only used them for two months, so this section is a guess more than a conclusion. I am not rewriting anything for it. New shared behaviour gets written as a hook first, and the component on top of it stays thin.
What it comes down to
The library is not there to make things possible. The framework already makes everything possible. It is there to make a small number of things easy and the rest noticeably inconvenient, so that eleven people building separate screens end up with one product.
I did not expect a job about buttons to turn into a job about saying no. But that is an API design problem, and the usual rules hold: small surface, explicit versions, slow removals, and a refusal to add what only one caller wants.