Design Tools

Component Libraries That Drift Away from the App

Somewhere in a repository there is a button component that has been carefully tested against forty-seven shades of gray. It has a loading state, a disabled state, a focus ring that meets contrast requirements, and a full changelog going back three years. Meanwhile, the app it lives in has drifted so far from that button that the button now looks like a stranger in its own house. The component library kept shipping. The application kept changing. Nobody noticed the gap until a designer opened a screen and asked why the primary action looked like it belonged to a different product.

This is the quiet failure mode of component libraries. They do not usually break. They rot. The code compiles, the tests pass, the version number ticks upward, and the distance between what the library offers and what the app actually needs grows a little wider each sprint until the library is a beautifully maintained museum of components nobody uses.

The library becomes a product with its own customers

A component library starts as a tool for one app. Then it gets a team, a roadmap, a Slack channel, and a release cadence. At some point it acquires users beyond the original app: a second product, an internal admin tool, a marketing site. Each new consumer files requests. Each request pulls the library slightly away from the app that birthed it.

The library team is not being unreasonable. They are responding to the people who show up in their channel. The trouble is that the original app's team stopped showing up. They built what they needed, moved on, and now find a library that has been shaped by other hands for eighteen months.

What was once a shared vocabulary becomes a negotiation. The button grows a new prop for a payment flow in a different product. The modal gains a variant for an onboarding sequence the original app never had. None of this is wrong. All of it adds weight the original app now carries for no reason.

The escape hatch is always open

Every component library has a way out. A wrapper, an override, a one-off style file, a local fork. The escape hatch is what keeps teams shipping when the library cannot move fast enough. It is also what kills the library slowly.

One team needs a date picker with a specific range behavior. The library's date picker is close but not quite right. Rather than wait for a PR to land, the team wraps it, adds forty lines of local logic, and moves on. Six months later, three teams have wrapped the same picker in three different ways. The library still ships its original version. The app now runs on a patchwork of wrappers that nobody owns.

The escape hatch is necessary. Without it, teams grind to a halt. But an escape hatch with no accounting becomes the default path, and the library becomes a suggestion box rather than a foundation.

Design tokens drift first, then components follow

The drift usually starts in the tokens. A spacing scale gets a new step. A brand refresh introduces a second accent color. The library updates its tokens. The app updates its tokens. But the two updates happen in different quarters, with different interpretations, and the mapping between them is maintained by hand.

A designer opens a screen and the padding looks off by four pixels. Nobody can say whether the library or the app is wrong. Both are following their own source of truth. The tokens were supposed to be the source of truth, but now there are two token files and a spreadsheet that explains how they relate.

Components inherit this ambiguity. A card that uses the library's spacing token looks cramped in the app because the app's surrounding layout uses different margins. The component is not broken. Its context has shifted underneath it.

Accessibility fixes land in one place and not the other

An accessibility audit flags a focus trap in a modal. The library team fixes it in the library. The fix ships in version 4.2.0. The app is on version 3.9.1 because a major version bump would require migrating a dozen components that were forked two years ago. The fix exists. It is not in production.

This is the sharpest edge of drift. When the library and the app diverge on accessibility, the divergence has real consequences for real people. A keyboard user hits a trap in the app's modal that was solved upstream months ago. The library's changelog reads like a list of solved problems the app cannot reach.

The gap is not caused by negligence. It is caused by the cost of staying current. Every version bump is a small migration project. Teams defer it because the app keeps shipping features, and the migration never becomes urgent until someone files a bug that traces back to a fix sitting in a version the app never adopted.

The library optimizes for generality, the app needs specificity

A library serving multiple consumers must generalize. Its components need enough props, slots, and variants to cover cases it has not seen yet. That generality is a feature for the library and a tax for the app.

The app knows its own screens. It knows the checkout flow has exactly three states and the settings page never needs a destructive secondary action. The library cannot know this, so it offers a component with fourteen props, six of which the app will never touch. The app's developers read the prop table, pick the four they need, and move on. The other ten sit there, maintained, documented, tested, and irrelevant.

Over time, the app's usage of the library narrows to a small subset of what the library offers. The library grows. The app's actual surface area shrinks. The relationship becomes lopsided.

Documentation describes a product the app no longer is

Open the library's docs and there is a getting-started guide, a theming section, and a page for every component. The docs are accurate to the library. They are not accurate to the app.

A new engineer joins the team, reads the docs, and builds a screen using the library's recommended patterns. The screen looks wrong. Not broken, just off: the spacing is different, the type scale does not match the rest of the app, the button hierarchy is inverted. The engineer learns that the docs describe an ideal the app abandoned long ago, and the real patterns live in the heads of two senior engineers and a Figma file that is six months stale.

This is how drift compounds. Each new person pays the cost of rediscovering the gap. The library's documentation, meant to be the bridge, becomes a source of confusion.

A version number is not a relationship

Dependency management tools make drift feel handled. The package.json lists a version. Renovate opens a PR every week. The CI passes. The version bumps. Everything looks current.

But a version number only says which snapshot the app points at. It says nothing about whether the app's usage matches the library's intent. An app can be on the latest version and still be drifted, because the drift lives in the wrappers, the overrides, the local forks, and the undocumented patterns that grew up around the library rather than inside it.

The healthiest component libraries are the ones where the consuming app's team has a standing relationship with the library team: a shared channel, a regular review of what the app actually uses, and a willingness to delete library features nobody consumes. That relationship is unglamorous and hard to schedule. It is also the only thing that keeps the library from becoming a separate product.

The app that stopped needing the library

There is a final stage of drift where the app quietly stops using the library for anything new. New screens are built with local components. The library is still imported, still in the dependency tree, still rendering the legacy surfaces nobody has touched in two years. But every new feature bypasses it.

The library team notices the drop in usage and asks what happened. The answer is not dramatic. The library could not move fast enough for a redesign, or it did not support the interaction the new feature needed, or the escape hatches had accumulated to the point where building fresh was cheaper than untangling. The app did not decide to leave. It just stopped coming back.

At that point, the library is maintenance without a customer. The components still work. The tests still pass. The release notes still go out. The app has moved on, and the library is a well-documented artifact of an earlier era, kept alive by inertia and the fact that removing it feels riskier than leaving it.

What the drift costs when a new screen ships

The cost shows up as a specific kind of friction. A designer specs a new screen using the library's components. An engineer builds it. The result does not match the spec because the library's component, in the app's context, does not render the way the design tool shows it. There is a meeting. The designer updates the spec to match reality. The library is not changed. The app is not changed. The gap is documented in a Figma comment and forgotten.

Multiply that meeting by every screen. The drift is not one big break. It is a thousand small reconciliations, each one teaching the team that the library is approximate. The library's authority erodes not through a decision but through a habit of working around it.

By the time someone proposes a cleanup, the proposal has to account for every workaround, every wrapper, every local pattern that replaced a library component. The cleanup is scoped as a migration. The migration is scoped as a quarter. The quarter gets deferred because the app needs to ship a feature. The drift continues.

The button with forty-seven shades of gray keeps shipping, immaculate and untouched, while the app it was built for renders a slightly different button that nobody planned and everyone uses.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button