veilcode
BlogDesign
Design

Design Systems That Don't Become a Burden

Most design systems start as a solution and end up as a problem. Here's how we build component libraries that teams actually want to use — and keep using.

AN

Aisha Nakato

Creative Director

19 August 20248 min read
Design SystemsTailwind CSSReactComponent Design
Figma file showing a component library with tokens, variants, and documentation
Share

The graveyard of enterprise software is full of design systems that were built with good intentions and abandoned within 18 months. The teams that built them are still proud of the Storybook. The teams that were supposed to use them went back to building their own buttons.

We've designed component libraries for 8 clients over the past 3 years. Here's what separates the ones that get adopted from the ones that get abandoned.

Why Most Design Systems Fail

Design systems fail for one of three reasons: they're built by designers who don't consider developer experience, they're built by engineers who don't consider design intent, or they're built for the system rather than for the product. The common thread is that they optimise for the wrong audience.

The adoption test: A component should be faster to use from the system than to build from scratch. If it isn't — if the API is confusing, the documentation is missing, or the customisation model is inflexible — developers will build their own. Every time.

Start With Tokens, Not Components

The biggest mistake teams make is jumping straight to building buttons and cards. Tokens — colour, spacing, typography, shadow — are the foundation everything else inherits from. If you get tokens wrong, every component built on top of them carries the mistake.

CSS
class=class="text-svc-data">"text-ink-faint italic">/* Good token architecture — semantic, not literal */
@theme {
  class=class="text-svc-data">"text-ink-faint italic">/* Semantic colour tokens */
  --color-base:      #080A0F;  class=class="text-svc-data">"text-ink-faint italic">/* page background */
  --color-surface:   #0D1117;  class=class="text-svc-data">"text-ink-faint italic">/* card background  */
  --color-ink:       #F0F4F8;  class=class="text-svc-data">"text-ink-faint italic">/* primary text     */
  --color-ink-muted: #8A9BB0;  class=class="text-svc-data">"text-ink-faint italic">/* secondary text   */
  --color-brand:     #00E5FF;  class=class="text-svc-data">"text-ink-faint italic">/* primary accent   */

  class=class="text-svc-data">"text-ink-faint italic">/* Spacing scale — consistent rhythm */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 40px;
}

Component API Design

The API of a component is a contract. Once teams are using it in production, changing it is expensive. Spend time on API design before writing implementation. The questions to answer are: What are the required props? What are the optional ones? What are the escape hatches?

  • Prefer composition over configuration — <Button> with <ButtonIcon> beats a single component with 12 boolean props
  • Always include a className override — never trap consumers inside your styles
  • Use TypeScript discriminated unions for variant props — prevents invalid state at compile time
  • Export both the component and its props type — consumers need to extend them
TSX
class=class="text-svc-data">"text-ink-faint italic">// ❌ Too many boolean props — combinatorial explosion
<Button primary disabled large iconLeft=class="text-svc-data">"arrow" iconRight=class="text-svc-data">"chevron" />

class=class="text-svc-data">"text-ink-faint italic">// ✅ Variant-based with composition for icons
<Button variant=class="text-svc-data">"primary" size=class="text-svc-data">"lg">
  <ButtonIcon icon={ArrowIcon} placement=class="text-svc-data">"left" />
  Continue
</Button>

Documentation That Gets Read

Documentation that isn't read is worse than no documentation — it creates a false sense that the system is self-explanatory. The only documentation that gets read is documentation that's in context: in the IDE via TypeScript types, in the component file via JSDoc, and in examples that show real usage patterns rather than synthetic demos.

Governance Without Bureaucracy

Every design system needs a process for proposing new components and changing existing ones. But most governance processes are too heavy — they require RFCs, design reviews, and approval chains that take weeks. The result is that teams bypass the process and build locally.

Lightweight governance: A shared Slack channel, a 48-hour RFC period on a PR, and a named component owner who reviews within one business day. That's all the process you need for teams under 20 engineers.

Our Tailwind Approach

We use Tailwind v4 with a custom theme layer for all our client component libraries. The key insight is that Tailwind's utility classes are the implementation detail, not the API. The API is your component props. Consumers shouldn't need to know what Tailwind classes your Button uses internally.

TSX
class=class="text-svc-data">"text-ink-faint italic">// The component consumer sees this
<PrimaryButton onClick={handleSubmit}>
  Save Changes
</PrimaryButton>

class=class="text-svc-data">"text-ink-faint italic">// The implementation detail (invisible to consumers)
function PrimaryButton({ children, className = class="text-svc-data">"", ...props }) {
  return (
    <button
      className={class="text-svc-data">`px-class="text-svc-social">7 py-class="text-svc-social">3.5 rounded-md border border-brand text-brand
        hover:bg-brand hover:text-base transition-all duration-class="text-svc-social">200
        focus-visible:ring-class="text-svc-social">2 focus-visible:ring-brand ${className}`}
      {...props}
    >
      {children}
    </button>
  );
}

Measuring Adoption

A design system that isn't used is a failure, regardless of how well it's designed. Track adoption by running a codebase grep for your component imports monthly. The ratio of system components to locally-created components tells you whether teams trust the system. If that ratio is declining, investigate why before adding new components.

  1. 1Count component imports from the system vs local components monthly
  2. 2Track time-to-first-use for new developers (should be <1 day)
  3. 3Survey teams quarterly on what's missing and what's painful
  4. 4Treat declining adoption as a P1 — it means the system is drifting from product needs
VeilCode Newsletter

Stay Sharp on What Matters

Engineering deep-dives, design thinking, and practical AI — written for builders who care about craft. No fluff. No spray.

No spam. Unsubscribe any time.

Continue Reading

Related Articles

Engineering
DK

Building Fast Web Apps for Low-Bandwidth Markets

Performance optimisation isn't a nice-to-have when your users are on 3G in Kampala or Nairobi. Here's the exact stack and techniques we use to build sub-2s load times across East Africa.

14 November 20249 min read
Read Article
AI & Automation
RM

Shipping AI Agents to Production: What Nobody Tells You

Building an LLM prototype takes an afternoon. Getting it to production takes months. After shipping 6 agent systems this year, here's the gap between the demo and reality.

2 October 202412 min read
Read Article