Tailwind CSS: Components, Tokens, and a Practical Design System

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.extend to centralize brand tokens
  • Reduce duplication with @layer components and small UI primitives
  • Document team rules (order, prefix, token naming)

Table of contents

  1. Concepts: tokens, primitives, components
  2. Implementation: tailwind.config and global layers
  3. Advanced: plugins, dark mode, libraries
  4. Performance: @apply vs utility composition
  5. Real-world cases
  6. Troubleshooting
  7. 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, card across 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

ApproachProsWatchouts
Raw utilitiesEasy to track changes; JIT optimizesVerbose markup
@apply component classesCleaner HTML; closer to a design systemOver-abstracting without real reuse
CSS variables + TailwindGood for runtime themingNeed 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 uses bg-[var(--color-brand)].
  • Monorepo: put tailwind.preset.js in packages/ui; apps use presets: [require('@repo/ui/tailwind.preset')].
  • Astro/Next: include all package sources in content so 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.