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)
| Pattern | Occurrences | Action |
|---|---|---|
| Page hero title | 7 pages | <Heading variant="hero"> |
| Card title | 5+ components | <Heading variant="cardTitle"> |
| Card description | 5+ components | <Text variant="cardDescription"> |
| Section label | 5 sections | <Text variant="label"> |
| Metadata/timestamp | 4+ components | <Text variant="meta"> |
| Body paragraph | 6+ pages | <Text variant="body"> |
Patterns that appeared 1–2 times (leave inline)
| Pattern | Occurrences | Decision |
|---|---|---|
| About page name/title styling | 1 | Leave inline |
| FAQ question text | 1 component | Leave inline |
| Experience role badge text | 1 component | Leave inline |
| Breadcrumb current page | 1 component | Leave 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:
-
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. -
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.
-
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:
| Abstraction | Extract when... |
|---|---|
| UI component | 3+ files use the same element + class combo |
| Style constant | 3+ components share the same className string |
| Custom hook | 3+ components duplicate the same stateful logic |
| Utility function | 3+ 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.
