Chapter 14 of 14 · Tailwind CSS for Beginners — Complete Free Tutorial

Theming and Optimizing Your Build

The last chapter of the Tailwind CSS for Beginners series brings everything together: defining your design as tokens instead of scattered values, switching the whole interface to dark mode, and shipping the smallest possible stylesheet. This is also where v4's biggest change lives — configuration is no longer a JavaScript file but ordinary CSS in your entry stylesheet.

Theme variables and their namespaces

Every value Tailwind knows comes from a theme variable, and variables are grouped into namespaces. The namespace decides which utilities a variable generates, so the name you choose has real consequences:

NamespaceGeneratesExample
--color-*Colour utilitiesbg-brand-500, text-mint-700
--font-*Font family utilitiesfont-display
--text-*Type sizes (and their line heights)text-xl
--breakpoint-*Responsive variants3xl:
--spacingEvery spacing, size and gap stepp-5, gap-8
--radius-*, --shadow-*Corner and elevation scalesrounded-brand, shadow-card

Because the variables are real CSS custom properties, everything you define is also readable at runtime — getComputedStyle(document.documentElement).getPropertyValue("--color-brand-500") hands the same token to JavaScript without a build-time export step.

Extending, overriding and resetting

Adding a token extends the default theme; redefining one overrides it; and a special value clears a whole namespace when your brand system should be the only option:

@import "tailwindcss";

@theme {
  --font-display: "Satoshi", sans-serif;
  --color-brand-50: oklch(0.97 0.02 240);
  --color-brand-600: oklch(0.51 0.2 240);
  --breakpoint-3xl: 112.5rem;
}

@theme inline {
  --font-sans: var(--font-inter);
}

Expected output:

New classes appear immediately: font-display, bg-brand-50,
text-brand-600 and the 3xl: breakpoint all compile. The inline block
makes the sans utility copy the value of --font-inter directly,
which keeps it correct when the variable is set on a subtree.

Use @theme inline whenever a token refers to another variable — it prevents the classic resolution problem where var() is evaluated where the variable is declared rather than where it is used. And @theme static forces every variable to be emitted even when no utility uses it, which is occasionally needed by scripts reading tokens at runtime.

Dark mode

v4 follows the operating system by default: the dark: variant responds to prefers-color-scheme with no configuration at all. Applications that expose a light/dark toggle instead switch to a class strategy with one custom variant:

/* in your entry CSS */
@custom-variant dark (&:where(.dark, .dark *));
<body class="bg-white text-slate-900 dark:bg-slate-950 dark:text-slate-100">
  <article class="rounded-xl bg-white shadow-sm dark:bg-slate-900 dark:shadow-none dark:ring-1 dark:ring-white/10">
    Same markup, two themes
  </article>
</body>

Expected output:

Unticked, a white page holds a white card with a soft shadow.
Toggle the .dark class on the body and the page turns near-black, the
card becomes dark grey, the shadow disappears and a faint light
outline takes its place.

Whichever strategy you pick, be consistent: mixing system-preference and class control in one project produces themes that disagree with each other.

Building for production

Development wants speed and feedback; deployment wants a small, finished file. The same tool does both, with flags deciding which mode you are in:

# during development: rebuild on every save
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch

# for deployment: one pass, minified
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --minify

Expected output:

The watch command prints each rebuild and its size as you save. The
minified build writes a single compact file — typically a few
kilobytes for a modest site — ready to be cached aggressively.

Whatever your bundler, three habits keep the output honest: confirm the build is part of your deploy step rather than a manual command, cache the result with a content hash in the filename, and check the size after large features instead of assuming it stayed small.

Sharing a theme across projects

Because tokens are plain CSS, a design system becomes a file rather than a package: move the @theme block into theme.css and import it from each project's entry file after @import "tailwindcss";. A monorepo can point straight at the shared file, and a published package works the same way. This replaces the v3 pattern of exporting a JavaScript config and calling resolveConfig in application code — now a dashboard, a marketing site and an admin tool can read the same brand values while still compiling three small, independent stylesheets.

Keeping the stylesheet small

Since only used classes are generated, size is mostly a question of scanning correctness and discipline:

  • Watch the scanner — classes hidden inside string concatenation or machine-generated markup need @source or @source inline("...") declarations.
  • Trim what you do not use — a namespace set to initial inside @theme removes its default tokens, which also removes their utilities from every future build.
  • Avoid transition-all and inline style attributes — they invite properties nobody asked for and defeat the "nothing unused" promise.
  • Measure, do not guess — search the compiled file for a class you removed and confirm it is gone.

Common mistakes

  • Editing a config file that is never read — v4 ignores tailwind.config.js until you load it with @config.
  • Defining tokens under :root — plain variables never create utilities; @theme is what turns a value into classes.
  • Two dark-mode mechanisms at once — the media query and the class strategy will fight each other.
  • Skipping @theme inline — tokens that reference other variables resolve in the wrong scope and fall back unexpectedly.
  • Shipping dev output — an unminified, unhashed file in production costs bytes and cache hits for nothing.

Practice checklist

  1. Declare a brand colour family and rebuild a previous component using only those tokens.
  2. Add a --breakpoint-3xl and use it to widen a grid on large monitors.
  3. Implement dark mode with the class strategy and check every page for unreadable pairs.
  4. Run a minified build and compare its size with the development output.
  5. Delete a rarely used token and confirm its utilities disappear from the compiled CSS.

That completes the series — browse the full catalogue of tracks at the tutorials index to pick what you learn next.

Quiz: Theming and Optimizing Your Build

5 questions · 70% to pass

Frequently Asked Questions

Tailwind is a utility-first CSS framework: instead of writing custom classes like .card-title, you compose elements from small single-purpose utilities such as text-lg font-semibold text-slate-800. The styles ship as a generated stylesheet containing only the classes your project uses, so the CSS stays small and consistent.

Bootstrap ships a opinionated component layer (buttons, navbars with Bootstrap's own look) that you override with custom CSS. Tailwind ships primitive utilities and lets you design freely — two sites built with Tailwind can look completely different, while two Bootstrap sites tend to look related. You can still build components; you just compose them from utilities first.

You need the fundamentals: the box model, specificity, Flexbox, Grid, media queries and pseudo-classes. Tailwind does not replace CSS knowledge — it renames it. Understanding what padding, margin, gap and z-index mean makes every utility class obvious instead of something to memorise.

v4 is CSS-first: configuration lives in CSS with @import "tailwindcss" and @theme tokens instead of a JavaScript tailwind.config.js, the engine is rewritten in Rust for much faster builds, and modern CSS features (container queries, cascade layers) are built in. This tutorial teaches v4 and notes where v3 differed.

Long class lists are a real trade-off, and the tutorial covers two fixes: consistent ordering (layout → spacing → typography → colour) and small component classes for repeated combinations. The generated CSS contains only classes you actually use — Tailwind removes everything else at build time, so the stylesheet is typically smaller than an unused framework.

Three utilities cover most cases: mx-auto centres a fixed-width block horizontally, flex items-center justify-centre centres children inside a flex parent, and grid place-items-center does the same for grids. Chapter 8 and chapter 9 show both patterns with examples.

Yes — Tailwind is a build step that scans your template files for class names and generates CSS, so it works with any framework or none. It does not ship runtime JavaScript, and the classes look the same in JSX, Blade, PHP or static HTML files.

In v4 you define design tokens in CSS with @theme, for example --color-brand-500: #4f46e5; creates bg-brand-500, text-brand-500 and friends. For a one-off value you can always use arbitrary values like bg-[#0f172a], which need no configuration at all.

A variant prefixes a utility with the condition it applies under: hover:bg-indigo-600 on hover, focus:ring-2 while focused, dark:bg-slate-900 in dark mode, and sm: / md: / lg: at breakpoints. Variants stack, such as md:hover:text-lg, and chapter 11 covers the full pattern.

Yes. Tailwind compiles to a static stylesheet with no JavaScript requirement, and because it ships only used classes, pages stay fast — a Core Web Vitals win that helps SEO. Accessibility, semantic markup and good content remain your job; Tailwind only styles what you write.