Back to posts

The Art of Minimal Design Systems

How constraints breed creativity—building design systems that do more with less.

designcssui

We've all been there: a design system with 47 shades of blue, 12 font sizes, and spacing values that follow no discernible pattern. Let's talk about doing more with less.

The Problem with Excess

Modern CSS frameworks give us infinite flexibility. But infinite options lead to decision fatigue and inconsistency. Every color you add is another color someone might misuse.

Starting with Typography

A good type scale can be generated from a single ratio. Here's how I approach it:

css
:root {
  --font-size-base: 1rem;
  --font-scale-ratio: 1.25; /* Major third */
  
  --font-size-sm: calc(var(--font-size-base) / var(--font-scale-ratio));
  --font-size-lg: calc(var(--font-size-base) * var(--font-scale-ratio));
  --font-size-xl: calc(var(--font-size-lg) * var(--font-scale-ratio));
  --font-size-2xl: calc(var(--font-size-xl) * var(--font-scale-ratio));
}

This mathematical relationship creates visual harmony. The eye perceives the progression as natural because it is natural—following the same ratios found in music.

The 3-Color Rule

Most interfaces need only three colors:

  1. Background — Your canvas
  2. Foreground — Your content
  3. Accent — Your calls to action

Everything else is a variation of these:

css
:root {
  /* Core palette */
  --color-bg: #0a0a0a;
  --color-fg: #e5e5e5;
  --color-accent: #3b82f6;
  
  /* Derived values */
  --color-muted: color-mix(in srgb, var(--color-fg) 50%, transparent);
  --color-border: color-mix(in srgb, var(--color-fg) 15%, transparent);
  --color-surface: color-mix(in srgb, var(--color-fg) 5%, var(--color-bg));
}

The color-mix() function is incredibly powerful. One accent color can generate hover states, disabled states, and subtle variations automatically.

Spacing That Makes Sense

Spacing should follow a predictable pattern. I use powers of 2 multiplied by a base unit:

css
:root {
  --space-unit: 0.25rem; /* 4px */
  
  --space-1: calc(var(--space-unit) * 1);   /* 4px */
  --space-2: calc(var(--space-unit) * 2);   /* 8px */
  --space-3: calc(var(--space-unit) * 3);   /* 12px */
  --space-4: calc(var(--space-unit) * 4);   /* 16px */
  --space-6: calc(var(--space-unit) * 6);   /* 24px */
  --space-8: calc(var(--space-unit) * 8);   /* 32px */
  --space-12: calc(var(--space-unit) * 12); /* 48px */
  --space-16: calc(var(--space-unit) * 16); /* 64px */
}

Notice the intentional gaps (no --space-5). Constraints force intentional decisions.

Component Philosophy

When building components, start with the smallest possible API:

tsx
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
  size?: 'sm' | 'md' | 'lg';
}

export function Button({ 
  children, 
  variant = 'primary',
  size = 'md' 
}: ButtonProps) {
  return (
    <button className={`btn btn-${variant} btn-${size}`}>
      {children}
    </button>
  );
}

Three variants. Three sizes. Nine combinations. That's almost always enough. When someone asks for a fourth variant, ask why the existing three don't work.

The Benefits

Minimal systems are:

  • Faster to learn — New team members onboard quickly
  • Easier to maintain — Less code, fewer bugs
  • More consistent — Limited options mean uniform output
  • Better performing — Smaller CSS bundles

Conclusion

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
— Antoine de Saint-Exupéry

Start with less than you think you need. Add only when the pain of not having something exceeds the cost of maintaining it. Your future self will thank you.