Le Quang Lam
← All writing

Design system anti-patterns

June 27, 2026·5 min read·software

A catalog of common mistakes that erode a design system over time. Each entry: what it looks like, why it's harmful, and what to do instead.

1. Inline arbitrary values

Looks like: text-[15px], gap-[7px], text-[#3a3a3a], duration-[183ms].

Why harmful: arbitrary values bypass the scale entirely. They create visual inconsistency (off-by-one sizing nobody notices individually but feels wrong cumulatively), and they're invisible to any rebrand or scale adjustment.

Do instead: use the nearest scale value. If no scale value fits and the need is real, add a token. If it's truly one-off, accept that the scale is good enough — the visual difference between gap-1.5 and gap-[7px] is almost never worth the cost.

2. Hardcoded colors outside globals.css

Looks like: bg-zinc-100 in a component, style={{ color: '#1A1917' }}, border-gray-200.

Why harmful: dark mode breaks. Rebrands break. The component carries assumptions about the palette that aren't visible at the token layer.

Do instead: use semantic tokens (bg-muted, text-foreground, border-border). If no semantic token fits, add one — that's the signal that a new role exists.

3. Skipping the semantic tier

Looks like: components reference primitives directly (bg-gray-50, text-gray-950).

Why harmful: every component now needs to be edited during a rebrand. The whole point of tiers collapses.

Do instead: even on a small project, define --background, --foreground, --border, --muted. Five semantic tokens are enough to start; they save hours later.

4. The "god component" with 15 boolean props

Looks like:

<Button primary secondary outline ghost large small loading disabled fullWidth ... />

Why harmful: prop combinations explode (2¹⁵ states, most untested). Reading the component becomes archaeology. Tree-shaking suffers.

Do instead: use cva (class-variance-authority) for real variants (e.g. variant: 'primary' | 'secondary' | 'ghost', size: 'sm' | 'md' | 'lg'). For genuinely different shapes, split into separate components (IconButton, LinkButton).

5. Heading wrappers instead of base styles

Looks like: <Heading level={2}>About</Heading> everywhere instead of <h2>About</h2>.

Why harmful: breaks semantic HTML conventions, adds a layer of indirection, and signals that base styles weren't set up. Screen readers and SEO get worse, not better.

Do instead: style h1h6 in @layer base so a plain <h2> looks right. Reach for a wrapper only when non-heading text needs heading-like styling (rare).

6. !important to override tokens

Looks like: className="text-foreground !text-red-500" or bg-card-foreground !bg-yellow-200.

Why harmful: !important is a smell — it means the system fought you and you won by force. The next person hits the same fight.

Do instead: ask why the token doesn't fit. Add a variant, add a new semantic role, or refactor the component to accept the right prop.

7. Documenting after building

Looks like: "I'll write the design docs once everything stabilizes." (It never stabilizes.)

Why harmful: docs written retroactively miss the why of every decision. They also tend not to exist.

Do instead: build the /design route alongside the first three components. Update it as you add tokens. Treat it as part of "done", not as cleanup.

8. Premature tokenization

Looks like: tokens for things used exactly once (--hero-padding, --about-card-bg).

Why harmful: pollutes the namespace. Future contributors see --about-card-bg and wonder if they should use it on the about page or if it's something else. Tokens should encode roles, not locations.

Do instead: rule of three — don't tokenize until a value appears in three places. Until then, an inline arbitrary value (with a comment if non-obvious) is honest about its scope.

9. Mixing layout with content components

Looks like: <HeroIntro className="mt-12 mb-8"> — the component carries margin assumptions about its surroundings.

Why harmful: the component can't be reused in a different layout. Spacing becomes a tug-of-war between parent and child.

Do instead: components style themselves; the parent handles spacing (via flex-gap or a layout primitive like Stack). A component should look identical in isolation as it does in context.

10. Animating without tokens

Looks like: duration-[183ms] ease-[cubic-bezier(0.3,1,0.4,1)] scattered across components.

Why harmful: motion language fragments. Some components feel quick, others feel sluggish, with no system. Adjusting "the feel of the site" requires touching every file.

Do instead: define --duration-* and --ease-* early. Use them everywhere. When tuning, change the token, not the components.

11. Letting globals.css swallow everything

Looks like: prose styles, animation keyframes, utility overrides, and component-specific CSS all piled into globals.css over months.

Why harmful: the file becomes unreadable, merge conflicts spike, and "where do I put this?" has no good answer.

Do instead: split as it grows — tokens.css, prose.css, animations.css. The @import at the top of globals.css keeps the loading story simple while the source stays organized.

12. Treating shadcn components as untouchable

Looks like: never editing files in components/ui/, even when the default style clashes with the brand.

Why harmful: shadcn explicitly copies source into your repo so that you can edit it. Treating it as a black box means every page wraps shadcn components in your own styling layer, doubling the surface.

Do instead: edit shadcn components freely. They're yours. Match them to your tokens, remove variants you don't use, adjust defaults. Commit the changes like any other code.

13. Global transition on everything

Looks like: *, *::before, *::after { transition: all 250ms; }.

Why harmful: causes jank during page loads (every element animates from default to actual state), animates things that shouldn't (layout shifts, position changes), and is a known performance hit on large pages.

Do instead: scope global transitions to specific properties relevant for theme toggle (color, background-color, border-color, fill, stroke). For component-level motion, apply transitions on the specific component.

14. Building a feature before its primitives

Looks like: writing a complex page with inline custom layouts, then later trying to extract primitives from it.

Why harmful: the extracted primitives are shaped by one use case and don't generalize. You end up with <SpecialHeroLayout> instead of <Stack>.

Do instead: when starting a feature, identify the layout primitives it needs. Build the primitives first (even minimal versions). Compose the feature from them.

15. Keeping tokens "just in case"

Looks like: --legacy-blue, --old-radius, --shadow-deprecated — tokens nobody references but nobody dares delete.

Why harmful: contributors don't know which tokens are real. Search results are polluted. The design surface looks larger and more confusing than it is.

Do instead: delete unused tokens. Git history is the safety net. If something breaks, you'll know within minutes.