Tailwind CSS Introduction

The utility-first philosophy versus custom CSS, installing Tailwind with Vite/PostCSS, and a first component.

Utility-first, not component-first

Traditional CSS (or a framework like Bootstrap) works by writing custom class names tied to what the component is.card, .navbar, .btn-primary — and defining their appearance in a separate stylesheet. Tailwind CSS takes a different approach: instead of naming components, you compose their appearance directly in markup using small, single-purpose utility classes, each of which maps to exactly one CSS declaration.

HTML
<!-- Traditional custom CSS -->
<button class="btn-primary">Save</button>
<style>
    .btn-primary {
        background-color: #3b82f6;
        color: white;
        padding: 0.5rem 1rem;
        border-radius: 0.375rem;
        font-weight: 600;
    }
</style>
HTML
<!-- Tailwind: the same result, no separate stylesheet rule needed -->
<button class="bg-blue-500 text-white px-4 py-2 rounded-md font-semibold">
    Save
</button>

Every class does one small thing — bg-blue-500 sets a background color, px-4 sets horizontal padding, rounded-md sets a border radius — and you build up the final design by combining many of them directly on the element.

Why utility-first, and the trade-off

The upside: you never have to invent a class name, switch to a separate CSS file, or worry about a change to .card accidentally affecting the fifteen other places .card is used — every element's styling is fully local to that element's class attribute. Tailwind's build step also strips out every utility class you never actually used, so shipped CSS stays small regardless of how large the utility set is.

The trade-off: markup gets visually denser, since styling information that used to live in a separate CSS file is now inline in the class attribute. Most teams find this pays off quickly — there's no context-switching between an HTML file and a CSS file, and no dead, unused CSS accumulating over a project's lifetime — but it is a real adjustment coming from traditional CSS.

Installing Tailwind (Vite + PostCSS)

Modern Tailwind projects (v3) install as a PostCSS plugin, most commonly wired up through Vite:

Bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

This generates two files: tailwind.config.js and postcss.config.js.

Javascript
// tailwind.config.js
module.exports = {
    content: [
        "./index.html",
        "./src/**/*.{js,jsx,ts,tsx,vue}",
    ],
    theme: {
        extend: {},
    },
    plugins: [],
};

The content array is critical — Tailwind scans these files at build time to figure out which utility classes are actually used, and generates CSS containing only those classes. Anything not listed in content won't be scanned, and any class used only in an unlisted file simply won't be generated.

Then create a CSS entry file that pulls in Tailwind's layers:

Css
/* src/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Javascript
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    css: {
        postcss: './postcss.config.js',
    },
});

Import the stylesheet once in your app's entry point, and every utility class becomes available across your templates.

Your first component

HTML
<div class="max-w-sm mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6">
    <h2 class="text-xl font-bold text-gray-900">Pro Plan</h2>
    <p class="mt-2 text-gray-600">Everything you need to scale.</p>
    <button class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 rounded-lg transition">
        Get started
    </button>
</div>

Reading this markup, the design is fully legible without opening a separate stylesheet: a white, rounded, shadowed card with padding, a bold heading, gray body text, and a full-width blue button that darkens on hover. That's the core pitch of utility-first CSS — the styling is co-located with the exact element it affects.

Common mistakes

  • Skipping (or misconfiguring) the content array in tailwind.config.js — any file not listed there won't have its classes detected, and the corresponding CSS silently won't be generated in the production build (though it may still appear to work in dev mode, which uses a different, unpurged pipeline).
  • Building class strings dynamically with string concatenation (e.g. `text-${color}-500`) — Tailwind's build-time scanner looks for complete, literal class names in your source, so a name assembled from partial fragments at runtime is often never detected and never generated.
  • Fighting the utility-first model by immediately writing custom CSS classes for everything instead of learning the utility set — most of what looks like it "needs" a custom class already has a matching utility combination.