Daniel Howells

Design System Evolution

2025-12-01

The Materia design system, @materia/ui, has grown to 33 packages, and the most counterintuitive decision was making tight coupling a deliberate feature. In most monorepo advice, you hear "keep packages loosely coupled." For a design system, the opposite is true. Components should share a single source of truth for tokens, spacing, and color so tightly that changing one value propagates everywhere instantly. A design system that lets individual components drift from the core tokens isn't a system, it's a collection.

The gray-only palette was the constraint that forced the most interesting design thinking. Every neutral in the system maps to Tailwind's built-in gray scale. No slate, no zinc, no neutral, no stone. Just gray. This sounds limiting until you realize that eliminating color variation from the neutral palette forces you to create hierarchy through typography and spacing alone. When you can't reach for a slightly warmer gray to differentiate a sidebar from the main content, you have to use font weight, size, and whitespace to do the work. The result is cleaner. Components look cohesive because they literally cannot clash on neutral tones.

The component API design centers on semantic props over utility classes. Instead of className="flex gap-4 items-center", components expose <HStack gap="4" align="center">. Stack, HStack, and VStack handle layout composition. Heading and Text handle typography. Button variants handle interactive states. The goal is that app-level code almost never needs raw Tailwind classes. When I review code and see className in an app component, it usually means @materia/ui is missing a primitive, which is a signal to add one rather than work around it. The system grows from real needs, not speculative abstractions.

The tight coupling pays off during refactors. When I updated the spacing scale last month, every component in every app picked up the change through a single token update. No find-and-replace across files, no "migration guide," no deprecation period. The current code is always canonical. Old patterns get removed, not deprecated. That philosophy means the system stays small and coherent even as it grows. Thirty-three packages sounds like a lot, but each one has a clear boundary and a single responsibility. The package count reflects domain complexity, not organizational sprawl.