Skip to content
Dazvix
Journal
Design systems4 min read

Designing a component system your whole team can trust

Most design systems fail quietly: people stop reaching for them. That is a trust problem, not a tooling problem.

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.

Tokens are the contract, not the palette

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 after the job, not the appearance

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 className and style overrides, 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.

This is the highest-leverage accessibility work available. Fixing one shared component fixes every screen that uses it, forever, including the ones nobody has built yet.

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

The fastest way to lose a team is to change a shared component and break three products without warning. Semantic versioning, a changelog written for humans, and deprecation that overlaps rather than cuts over: mark the old API deprecated, ship the new one alongside, give people a release or two, then remove it.

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 is not a library you ship. It is an agreement you maintain, and it lasts exactly as long as people trust it to cover their case.