Skip to main content

The Rule of Three in Design Systems

Three identical geometric shapes casting different shadows
Apr 5, 20263 min readArchitecture, React, Component Library

The temptation to abstract early

You know the feeling: you write a 90-character Tailwind class string, paste it into a second file, and your refactoring instinct kicks in. "This should be a component."

But pulling it out early just trades one kind of debt for another. A shared component with one or two consumers is harder to change than the inline code it replaced; you have to reason about everywhere it's used, keep its props API honest, and write tests for it. And when the two usages drift apart (they usually do), you're stuck maintaining an abstraction that serves neither one well.

The Rule of Three is a blunt little heuristic: don't extract until a pattern shows up at least three times. Below three, the cost of the abstraction outweighs the cost of just repeating yourself.

The typography audit

I went through every heading and text pattern across a 20-page portfolio site, and the findings split cleanly down the middle:

Patterns that repeated 3+ times (extract)

PatternOccurrencesAction
Page hero title7 pages<Heading variant="hero">
Card title5+ components<Heading variant="cardTitle">
Card description5+ components<Text variant="cardDescription">
Section label5 sections<Text variant="label">
Metadata/timestamp4+ components<Text variant="meta">
Body paragraph6+ pages<Text variant="body">

Patterns that appeared 1–2 times (leave inline)

PatternOccurrencesDecision
About page name/title styling1Leave inline
FAQ question text1 componentLeave inline
Experience role badge text1 componentLeave inline
Breadcrumb current page1 componentLeave inline

The one-off patterns are tied to their context. Extracting "FAQ question text" into a variant would give me a variant used by exactly one file: pure overhead with nothing to show for it.

When the rule bends

There are a few spots where extracting before the third occurrence is worth it:

  1. Safety-critical patterns: Form error messages (text-sm text-error) only showed up twice, but they carry a clear a11y contract and consistent error display matters, so I extracted <Text variant="error"> anyway.

  2. Cross-boundary patterns: A pattern used once in the app and once in shared-ui is worth extracting, because divergence costs more across a library boundary than it does inside one app.

  3. Complex patterns: If even a single occurrence stacks up 5+ coordinated classes with responsive breakpoints and dark mode variants, the odds of copy-paste drift are high enough that I'll extract it early.

Applying it beyond typography

The same rule holds for any design system decision:

AbstractionExtract when...
UI component3+ files use the same element + class combo
Style constant3+ components share the same className string
Custom hook3+ components duplicate the same stateful logic
Utility function3+ call sites with the same transformation

The key insight: three is the smallest number where a pattern proves it's stable. Two occurrences might have lined up by coincidence; three occurrences mean there's a real shared concept worth naming and maintaining.

The counter-argument

Some teams extract everything into a design system from day one, and that works when you've got a mature design spec full of validated patterns. But for products that are still moving (startups, portfolio sites, MVPs) the spec is the code, and extracting early just freezes patterns before they've earned it.

Three similar lines of code beat a premature abstraction every time. Let the pattern settle first, then extract it with confidence.