A design system does not fail with an announcement. It fails when a developer needs a slightly different button, finds that the shared one cannot do it, and writes a local one instead. Six months later there are fourteen buttons, the library is stale, and someone proposes a rewrite.
The usual diagnosis is that the team lacked discipline. It is almost always that the system was not trustworthy: it did not cover the real cases, it was harder to use than writing CSS, or it changed underneath people without warning. Those are design problems, and they are fixable.
How tokens guide interface changes
The most common mistake is tokens that name appearance. --blue-500 tells you what a value looks like today and nothing about when to use it, so the moment the brand shifts or a dark theme arrives, every usage needs review.
Tokens should name intent and resolve to primitives in one place. That gives you a layer to re-theme without touching a single component.
:root {
/* Primitives, raw values, never referenced by components. */
--blue-500: #3f6bff;
--grey-950: #0b0b0d;
--grey-100: #ece8df;
/* Semantic tokens, what components actually use. */
--surface-page: var(--grey-950);
--surface-raised: #101014;
--text-primary: var(--grey-100);
--text-secondary: #8b8b84;
--action-primary: var(--blue-500);
--border-hairline: #23232c;
}
/* Re-theming touches this block and nothing else. */
[data-theme='light'] {
--surface-page: #f7f6f2;
--surface-raised: #ffffff;
--text-primary: #17171c;
--text-secondary: #63625b;
--action-primary: #2f57f0;
--border-hairline: #e4e2da;
}The rule that makes this hold: a component may never reference a primitive. If a component needs a colour that has no semantic token, that is a signal the system is missing a concept, which is a five-minute conversation, not a reason to hardcode a hex value.
Name components by their role
A component called BlueCard is obsolete the day someone needs a grey one. A component called ProjectCard survives a rebrand. The test is whether the name would still be correct if the design changed completely, if not, it is describing the wrong layer.
The same applies to variants. variant="primary" describes hierarchy and will outlive a palette change. variant="blue" describes pigment and will not.
Design the API before the pixels
The prop signature is the part people live with. Two failure modes recur:
- Too rigid, the component covers 80% of cases and offers no escape hatch, so the remaining 20% get forked. Every fork is a future inconsistency.
- Too loose, the component accepts arbitrary
classNameandstyleoverrides, so it enforces nothing and the system becomes decorative.
The middle is a small set of well-chosen variants, plus composition through children for the genuinely bespoke parts. Where an escape hatch is needed, make it explicit and greppable rather than pretending it does not exist.
type ButtonProps = {
/** Hierarchy, not colour, survives a rebrand. */
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
/** Renders as <a> when href is present, <button> otherwise. */
href?: string;
loading?: boolean;
children: React.ReactNode;
};
// No `className` prop. If a caller needs a shape this cannot produce,
// that is a system gap worth a conversation, not a one-off override.Accessibility is part of the component, not the caller
If every consumer has to remember the aria- attributes, most will not, and the audit at the end of the project becomes a scavenger hunt. Bake it in: focus management in the modal, aria-expanded on the disclosure, a visible focus ring that survives a theme change, aria-live on the async region.
A shared component fixes an accessibility issue once for every screen that uses it, including screens the team adds later.
Documentation is where trust is won
A props table generated from types is necessary and nowhere near sufficient. The questions that actually send someone off to write their own component are:
- When do I use this instead of that one?
- What does it look like with a 90-character label, or none?
- What happens while it is loading, empty, or errored?
- Can I put it inside a card? Inside a form?
- What is deliberately not supported, and what should I use instead?
Document the edges, the long string, the empty state, the failure, because those are exactly the cases that drive people to fork. A component whose documented behaviour matches what happens at 2am on a Friday is one people keep reaching for.
Version it like the dependency it is
Changes to a shared component can break products that depend on it. Use semantic versioning, maintain a readable changelog, and keep deprecated APIs available for a release or two while teams move to the replacement.
Visual regression tests make this safe to do often. A screenshot diff on every pull request catches the unintended two-pixel shift that nobody would have caught in review, which is what lets you keep evolving the system instead of freezing it.
The signal to watch
Adoption is the only metric that matters, and it is easy to measure: count the local, one-off components in the product repos. If that number is falling, the system is working. If it is rising, something about the system is harder to use than writing it fresh, and no amount of documentation will fix that until you find out what.
A design system needs ongoing maintenance. Teams use it when its components cover their needs and its guidance stays current.