Tailwind CSS: Components, Tokens, and a Practical Design System
이 글의 핵심
Centralize design tokens in tailwind.config, organize @layer components, and structure Tailwind projects to reduce duplicated classes.
Introduction
Without a deliberate Tailwind CSS project structure, piling on @apply and arbitrary hex colors leads to “why does this button have three sizes?” months later. Even without a formal design system, tokens (color, spacing, type) + a component class layer cuts maintenance cost sharply.
This post covers Tailwind v4 (2026, @import "tailwindcss") and v3-style tailwind.config customization, plus folder, layer, and naming patterns that work in production.
Early sprints favor raw utilities for speed, but missing the moment to introduce tokens and a component layer makes refactors expensive. Below we align when to promote patterns to shared layers.
After reading this post
- Use
theme.extendto centralize brand tokens - Reduce duplication with
@layer componentsand small UI primitives - Document team rules (order, prefix, token naming)
Table of contents
- Concepts: tokens, primitives, components
- Implementation:
tailwind.configand global layers - Advanced: plugins, dark mode, libraries
- Performance:
@applyvs utility composition - Real-world cases
- Troubleshooting
- Wrap-up
1. Concepts: tokens, primitives, components
- Design tokens: meaningful names → values for palette, spacing scale, radius, font sizes and line heights.
- Primitives: minimal reusable styles like
btn,input,cardacross screens. - Page/feature components: primitives composed into React/Vue/Svelte components—business logic meets style here.
Tailwind’s strength is experiment with utilities, then promote stable patterns to tokens and @layer.
Implementation
2-1. tailwind.config.js — centralize tokens with theme.extend
// tailwind.config.js (v3-style example)
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx,astro,vue,svelte}'],
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
500: '#3b82f6',
900: '#1e3a8a',
},
surface: {
DEFAULT: '#ffffff',
muted: '#f4f4f5',
},
},
spacing: {
18: '4.5rem',
22: '5.5rem',
},
borderRadius: {
card: '0.75rem',
},
fontSize: {
display: ['2.25rem', { lineHeight: '2.5rem', fontWeight: '700' }],
},
},
},
plugins: [],
};
2-2. Global CSS — fix layer order
/* src/styles/globals.css — v4 example */
@import "tailwindcss";
@layer components {
.btn {
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium
transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2;
}
.btn-primary {
@apply btn bg-brand-500 text-white hover:bg-brand-900 focus-visible:outline-brand-500;
}
.btn-ghost {
@apply btn bg-transparent text-brand-500 hover:bg-brand-50;
}
}
2-3. Usage in components
// Button.tsx
export function Button({
variant = 'primary',
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'ghost' }) {
const cls =
variant === 'primary' ? 'btn-primary' : 'btn-ghost';
return <button type="button" className={cls} {...props} />;
}
Example rule: components/ui/ holds primitives only; features/ holds domain components—prefer @apply on primitives and short class strings on pages.
Advanced
Dark mode: class strategy + tokens
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
};
<div className="bg-surface text-zinc-900 dark:bg-zinc-900 dark:text-zinc-50">
...
</div>
Tip: more semantic tokens like surface reduce repeated dark: prefixes.
Plugins such as @tailwindcss/forms
Form resets affect the whole team—agree once and document interaction with preflight.
4. Performance: @apply vs utility composition
| Approach | Pros | Watchouts |
|---|---|---|
| Raw utilities | Easy to track changes; JIT optimizes | Verbose markup |
@apply component classes | Cleaner HTML; closer to a design system | Over-abstracting without real reuse |
| CSS variables + Tailwind | Good for runtime theming | Need naming and fallback policy |
Practical rule: promote to @apply only when the same five+ utility lines repeat three+ times.
Real-world cases
- Multi-brand:
data-theme="acme"+ CSS variables for color tokens; Tailwind usesbg-[var(--color-brand)]. - Monorepo: put
tailwind.preset.jsinpackages/ui; apps usepresets: [require('@repo/ui/tailwind.preset')]. - Astro/Next: include all package sources in
contentso classes are not dropped.
Troubleshooting
JIT misses classes
Dynamic string concatenation ('text-' + color) is not statically analyzable—use safelist or a full class name map.
Arbitrary values break inside @apply
Check plugin order and definitions outside @layer. Sometimes direct utilities on the component are safer.
Figma tokens do not match numbers
A Figma → JSON → Style Dictionary → Tailwind theme pipeline reduces drift.
Wrap-up
Tailwind project structure is less about fancy folders than single source of truth for tokens and team-agreed layer rules. Pair with HTML/CSS series posts on animation and responsive layout for end-to-end consistency.