본문으로 건너뛰기
Previous
Next
Tailwind CSS: Components, Tokens, and a Practical Design

Tailwind CSS: Components, Tokens, and a Practical Design

Tailwind CSS: Components, Tokens, and a Practical Design

이 글의 핵심

Understand how Tailwind behaves inside the PostCSS pipeline (JIT, content scanning), then align design tokens, layers, and production component patterns in one coherent workflow.

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 article covers Tailwind v4 (2026, @import "tailwindcss") and v3-style tailwind.config customization, and connects PostCSS, the JIT engine, and content scanning?�not just folder names?�so you can explain what happens when styles are built and avoid ?�production-only??surprises.

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 and how scanning and tokens interact.

Why tokens and layers?

Tailwind helps you ship UI quickly with utilities. As projects grow, hex codes and spacing numbers scattered in JSX force global search-and-replace for design updates. theme.extend and @layer centralize meaningful names so brand refreshes and dark mode cost less.

Production watchouts

  • Dynamic class strings ('text-' + color) may not be static analysis?�friendly?�expect missing styles in production. Use safelist or a full class-name map.
  • Too much @apply can add abstraction without reuse. A team rule (e.g. promote after three repeats of the same five utility lines) helps.
  • In monorepos, omitting package sources from content causes prod-only missing styles.

After reading

  • Know where Tailwind mutates the CSS AST in PostCSS.
  • Explain how JIT scans files and generates or omits utility rules.
  • Split primitive vs semantic tokens and pair theme.extend with CSS variables where appropriate.
  • Apply variant APIs, presets, and library boundaries with clear criteria.

Table of contents

  1. Concepts: tokens, primitives, components
  2. PostCSS plugin architecture and the build pipeline
  3. JIT compilation: utilities generated on demand
  4. Not ?�purge?�—content-driven generation and tree-shaking
  5. Design token system: primitive, semantic, runtime
  6. Implementation: tailwind.config and global layers
  7. Production component patterns
  8. Advanced: plugins, dark mode, component libraries
  9. Performance: @apply vs utility composition
  10. Real-world cases
  11. Troubleshooting
  12. 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??business logic meets style* here.

Tailwind?�s strength is experiment with utilities, then promote stable patterns to tokens and @layer. The sections below show how that promotion becomes reproducible CSS in the build pipeline.


2. PostCSS plugin architecture and the build pipeline

PostCSS parses CSS into an AST, runs a plugin chain in order, and serializes back to CSS. Tailwind is a plugin that understands @tailwind base/components/utilities or v4?�s @import "tailwindcss" and expands directives into the final stylesheet.

Why order matters

Typically you fix the meaning of source CSS first, then attach Autoprefixer, minifiers, etc. Teams differ on whether postcss-import merges files before Tailwind or a single entrypoint suffices, but the invariant is: Tailwind resolves @apply and @layer before downstream tools assume ?�final??rules. Wrong order can break nesting, variable scope, or cause subtle plugin conflicts.

Typical postcss.config (v3-style)

// postcss.config.js ??illustrative; align with your stack
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

In monorepos you may need to point the Tailwind plugin at a specific config file. When multiple configs exist, document which CSS entry maps to which config to avoid ?�works locally, different in CI.??

Tailwind v4 entrypoints and responsibility split

v4 often uses @import "tailwindcss"; in CSS. Internally there is still a pipeline that reads CSS, generates and merges rules, whether PostCSS or Lightning CSS / Vite drives it. The split remains source ??transform ??final asset; teams only need clarity on which tool interprets @apply and custom @theme.

In short: Tailwind is the engine that turns class candidates + config into CSS rules; PostCSS (or an equivalent adapter) places that engine in the same AST world as the rest of your stylesheets.

v4, Vite, and Lightning CSS

Many v4 templates attach @tailwindcss/vite or use Lightning CSS so a hand-written postcss.config is optional. Scan ??generate ??emit is unchanged; document whether your team?�s standard path goes through PostCSS or the Vite plugin so debugging stays predictable.


3. JIT compilation: utilities generated on demand

Older workflows pre-generated huge stylesheets and stripped unused rules. Today?�s default is closer to JIT: at dev and build time, Tailwind collects class candidates from project sources and emits only the utilities you need.

Scan ??collect ??generate

  1. Walk files matched by content (v3) or the file set your bundler provides.
  2. Extract strings parseable as Tailwind class syntax. Template literals, concatenation, and conditional rendering can hit static analysis limits?�candidates may be missed.
  3. For each candidate, apply theme, plugins, and variants (hover:, dark:, ?? to produce selectors and declarations.
  4. Cache incrementally in dev to keep rebuilds fast.

JIT is not ?�the browser composes classes at runtime?? it is build-time generation. Dynamic combinations that never appear as whole class names in scanned sources may not exist in the output.

v4 and moving configuration

Some options moved to @theme and @import in v4, but the engine still reacts to scan results. When migrating, prioritize scan paths and dynamic-class policy over whether a knob lived in JS or CSS.


4. Not ?�purge?�—content-driven generation and tree-shaking

PurgeCSS-style ?�build everything, delete unused??differs from JIT + content scanning, which avoids emitting most unused rules upfront. The feel is similar to JS tree-shaking: only what you need in the final artifact?�but the mechanism is candidate generation, not only deletion.

content is a whitelist of scan roots

The content array in tailwind.config.js defines where to look for class names. Classes that only exist outside those globs do not get generated. Conversely, if tests, Storybook, or package sources are omitted, styles visible in local stories can vanish in the app production build.

// tailwind.config.js ??monorepo paths often added
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx,astro,vue,svelte}',
    '../../packages/ui/src/**/*.{js,ts,tsx}',
  ],
  // ...
};

safelist and full class maps

For runtime-only classes (themes, CMS options), the scanner may never see them. Use safelist, patterns, or explicit maps of full class strings?�a classic cause of *?�broken only in production.??

Terminology

  • Legacy ?�purge??*: remove unused rules after a large build.
  • Content + JIT: generate needed rules; missing paths surface as missing utilities, not silent success.

Prefer content scanning and candidate generation in team docs over vague ?�purge.??

5. Design token system: primitive, semantic, runtime

Once tokens are structured, components and pages become explainable.

Primitive tokens

Source values: palette steps (brand.500), spacing scale. theme.extend (or @theme) fits build-time constants.

Semantic tokens

Role-based names: surface, muted, danger?�optimize for what it means, not which palette step it is. Helps dark mode and brand switches when only semantic layers change.

Runtime tokens (CSS variables)

For post-deploy theming or per-tenant colors, CSS custom properties are often the single source of truth; Tailwind bridges with bg-[var(--color-surface)]. Document fallbacks and scope (:root vs [data-theme]).

Boundary between tokens and components

Tokens store design decisions; components encode how tokens combine. Tokens without component rules sprawl variants; components without tokens duplicate meaningful-but-different class soups per page. Use both.


6. Implementation

6-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: [],
};

Keep primitive palettes like brand.*, but prefer consuming semantic tokens like surface in UI rules so palette updates propagate through semantic mapping.

6-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;
  }
}

@layer controls cascade order across utilities, components, and pages. If layer order drifts, you get *?�only this component looks wrong?? overrides.

6-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} />;
}

Rule of thumb: components/ui/ holds primitives; features/ holds domain components?�keep @apply on primitives; keep page class strings short.


7. Production component patterns

Variant APIs: avoid combinatorial explosion

When size × variant × iconOnly grows, centralize variant tables (the idea behind class-variance-authority). The win is one place owns combination rules, whether or not you adopt a specific library.

const buttonVariants = {
  primary: 'btn-primary',
  ghost: 'btn-ghost',
} as const;

export function Button({
  variant = 'primary',
  className,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: keyof typeof buttonVariants;
}) {
  return (
    <button
      type="button"
      className={[buttonVariants[variant], className].filter(Boolean).join(' ')}
      {...props}
    />
  );
}

Standardize tailwind-merge (or not) for conflicting utilities when parents override children.

Compound components and slots

For cards and dialogs, expose token-based primitive classes on each slot and inject content from parents?�consistency depends on every slot pulling from the same token set.

Design-system packages

For packages/ui, use peerDependencies on tailwindcss, ship tailwind.preset.js, and ensure UI package sources stay inside the app content globs?�otherwise JIT issues recur.

Production checklist

  • Include Storybook/doc templates in content when they introduce classes.
  • Handle dynamic classes with safelist or explicit maps.
  • Prefer semantic tokens for dark mode instead of dark: sprawl.
  • Watch for @apply conflicts with global layer order.

8. 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>

When dark: repeats everywhere, swapping semantic tokens via CSS variables (--color-surface) and a single bg-[var(--color-surface)] often scales better.

Plugins like @tailwindcss/forms

Form resets affect the whole team??agree once* and document interaction with preflight. Plugins inject rules and variants through the same PostCSS chain?�note debug order when conflicts appear.


9. Performance: @apply vs utility composition

ApproachProsWatchouts
Raw utilitiesEasy to trace; JIT-friendlyVerbose markup
@apply component classesCleaner markup; DS-friendlyAbstracting without reuse
CSS variables + TailwindRuntime themingNaming and fallback policy

Rule of thumb: consider @apply when the same five+ utility lines repeat three+ times. Most ?�performance??issues are scan misses or duplicate rules, not micro-optimizations of class count.


10. Real-world cases

  • Multi-brand: data-theme="acme" + CSS variables for colors; Tailwind uses bg-[var(--color-brand)].
  • Monorepo: tailwind.preset.js in packages/ui; apps use presets: [require('@repo/ui/tailwind.preset')].
  • Astro/Next: put all package sources in content to avoid missing utilities.

11. Troubleshooting

MistakeResultFix
String concatenation for classesMissing styles in prodFull class map or safelist
darkMode: 'class' but no root classDark styles never applyToggle dark on html or container
Heavy arbitrary values only in @applyBuild/plugin conflictsPrefer utilities or simplify
Figma vs code driftDouble maintenanceToken pipeline (Figma ??JSON ??theme)

JIT cannot find a class

Dynamic concatenation is not reliably parsed?�use safelist or complete class names.

Arbitrary values break in @apply

Check plugin order and @layer placement; sometimes direct utilities on the component are safer.

Figma tokens diverge

A Figma ??JSON ??Style Dictionary ??Tailwind theme pipeline reduces drift.


12. Wrap-up

Tailwind project structure is less about fancy folders than a single source of truth for tokens and team-agreed layer rules. Understanding PostCSS position, JIT, and content scanning lets you block production-only missing styles at design time. Pair with HTML/CSS series on animation and responsive layout for end-to-end consistency.


?�주 묻는 질문 (FAQ)

Q. ???�용???�무?�서 ?�제 ?�나??

A. Connect PostCSS, the JIT engine, and content scanning to tokens, layers, and production-ready component patterns in Tail???�무?�서????본문???�제?� ?�택 가?�드�?참고???�용?�면 ?�니??

Q. ?�행?�로 ?�으�?좋�? 글?�?

A. �?글 ?�단???�전 글 ?�는 관??글 링크�??�라가�??�서?��?배울 ???�습?�다. C++ ?�리�?목차?�서 ?�체 ?�름???�인?????�습?�다.

Q. ??깊이 공�??�려�?

A. cppreference?� ?�당 ?�이브러�?공식 문서�?참고?�세?? 글 말�???참고 ?�료 링크???�용?�면 좋습?�다.


같이 보면 좋�? 글 (?��? 링크)

??주제?� ?�결?�는 ?�른 글?�니??


??글?�서 ?�루???�워??(관??검?�어)

Tailwind CSS, Design System, Components, Design Tokens, PostCSS, JIT, Frontend ?�으�?검?�하?�면 ??글???��????�니??