Building a Component Library That Lasted Three Years
Most component libraries become maintenance burdens within a year. Ours lasted three. Here's what we did differently.
Why Most Libraries Fail
Most component libraries fail in the same way: they start strong, with a clean set of components that solve immediate needs, and they decay as the team adds components and features without a clear design philosophy. The decay shows up as inconsistent APIs (similar components with different prop names), overlapping components (three different Button components that each do slightly different things), and accumulated complexity (components with thirty props because every consumer wanted a slightly different behavior). By the time the decay is noticeable, fixing it requires a major version bump that breaks every consumer, and the team doesn't have the appetite for that, so the decay continues until someone proposes starting over from scratch — at which point the cycle begins again.
The root cause of the decay is that most libraries are built bottom-up — components are added as needed, with each component designed in isolation — rather than top-down, with a clear design philosophy that governs how all components should work. A bottom-up library is a collection of components; a top-down library is a system. The system is harder to build initially, because it requires deciding on conventions before you have enough components to see the patterns, but it's dramatically easier to maintain, because every new component has a clear model to follow rather than being designed from scratch. The three years our library lasted weren't because we built great components; they were because we built a great system that the components fit into.
The Design Philosophy
The design philosophy came down to three principles. First, composition over configuration: instead of a component with thirty props that cover every use case, we built small, focused components that compose together. A Modal isn't a component with `header`, `body`, and `footer` props; it's a Modal component that takes children, and the consumer composes ModalHeader, ModalBody, and ModalFooter inside it. Composition is more flexible (you can use any of the sub-components or none of them) and more readable (the JSX shows the structure rather than hiding it in props). Second, consistent API conventions: every component that accepts children uses the `children` prop, every component that has a size uses `size="sm" | "md" | "lg"`, every component that can be disabled uses `disabled`. Consistency makes the library learnable — once you know the convention, you can guess the API of any component.
Third, minimal surface area: every prop is a maintenance burden, because every prop has to be documented, tested, and supported across versions. We treated every new prop as a cost to be justified, not as a feature to be added, and we pushed back on prop requests that could be handled by composition or by the consumer wrapping the component. That pushback was uncomfortable in the moment — saying "no" to a teammate who wants a prop is harder than saying "yes" — but it kept the API surface manageable, which is what allowed the library to age well. A library with twenty components and five props each is maintainable; a library with twenty components and twenty props each is a maintenance nightmare.
What We'd Do Differently
The one thing we'd do differently is invest in automated testing earlier. The library had Storybook stories from the beginning, which served as visual documentation, but we didn't add automated component tests until the second year, and by then some components had accumulated subtle behaviors that the tests had to reverse-engineer rather than specify. Adding tests from the beginning would have locked in the expected behavior and made refactoring safer, because the tests would have caught any unintended behavior change. The cost of adding tests late is higher than the cost of adding them early, because late tests have to understand existing code while early tests define new code.
The other thing we'd do differently is versioning. We used semantic versioning, which was correct in principle, but we were too conservative about major version bumps — we accumulated breaking changes for two years before releasing v2, which made the migration painful for consumers. Smaller, more frequent major versions (with migration guides and codemods) would have been less painful than the accumulated big-bang migration we eventually required. The lesson is that versioning isn't just about following semver; it's about managing the consumer experience, and that sometimes means releasing a major version with a single breaking change rather than accumulating changes until the migration is a project.
