Tailwind CSS tutorial: from install to production in one sitting

# tutorials · css

Tailwind CSS tutorial: from install to production in one sitting.

Tailwind CSS is a utility-first CSS framework. Instead of writing CSS in a separate file, you compose styles from class names directly on the markup. This tutorial covers install (with both the standalone CLI and the Vite plugin), configuration, dark mode, arbitrary values, @apply, plugins, and a real migration story from vanilla CSS on a mid-sized site.

# who this is for

Who this tutorial is for.

Working developers who already write CSS and want to know when Tailwind is the right call and how to configure it without ending up with a tailwind.config.js the size of a novel. This is Tailwind 4.x; the config schema changed materially from 3.x, so 3.x tutorials will lead you astray.

If you are still learning CSS fundamentals (specificity, the cascade, flex vs grid), do that first. Tailwind speeds up developers who know CSS. It hides the model from developers who do not.

# install

Step 1: install Tailwind 4.

Tailwind 4 ships two install paths: the standalone CLI (for HTML sites, plain PHP, WordPress, or any build tool that is not Vite) and the first-party Vite plugin (for Vite, Next.js, Astro, Remix, SvelteKit).

Vite plugin path (recommended for React, Vue, Svelte, Astro)

pnpm add tailwindcss @tailwindcss/vite
# or npm install tailwindcss @tailwindcss/vite

Add the plugin to vite.config.ts:

import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [tailwindcss()],
});

Then, in your main CSS file (usually src/index.css or app/globals.css):

@import 'tailwindcss';

Standalone CLI path (WordPress, plain HTML, any non-Vite setup)

pnpm add -D tailwindcss @tailwindcss/cli
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch

That is the entire install. Tailwind 4 dropped the PostCSS-plugin ceremony that 3.x required.

# config

Step 2: configuration lives in CSS now.

The big change in Tailwind 4: tailwind.config.js is optional. Configuration lives in your CSS with the @theme directive. If you need to add a custom colour, extend the font stack, or set a breakpoint, it goes here:

@import 'tailwindcss';

@theme {
  /* custom colours */
  --color-brand-ink: #0B0E14;
  --color-brand-spark: #5CE1E6;
  --color-brand-amber: #F4B860;

  /* custom fonts */
  --font-display: 'Inter Tight', system-ui, sans-serif;
  --font-body: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', ui-monospace, monospace;

  /* custom breakpoints */
  --breakpoint-3xl: 1920px;
}

These variables become Tailwind utility classes automatically: bg-brand-ink, text-brand-spark, font-display, 3xl:text-xl. No JS config, no theme.extend, no content array (Tailwind 4 scans your source files automatically).

# real markup

Step 3: what a real component looks like.

Here is a card component in React with Tailwind classes. This is the pattern you will use hundreds of times a week: layout with flex / grid utilities, spacing with p- / m- / gap-, colour and typography from the theme.

export function ReviewCard({ host, verdict, blurb, href, price }: {
  host: string;
  verdict: string;
  blurb: string;
  href: string;
  price: string;
}) {
  return (
    <article className="flex flex-col gap-3 rounded-lg bg-slate-900 p-6 ring-1 ring-slate-700">
      <p className="font-mono text-xs uppercase tracking-wider text-amber-400">
        hosting review · {host}
      </p>
      <h3 className="text-xl font-semibold leading-tight text-slate-100">
        <a href={href} className="hover:text-cyan-300">{blurb}</a>
      </h3>
      <p className="font-mono text-sm text-emerald-400">▸ verdict {verdict}</p>
      <div className="mt-2 border-t border-slate-700 pt-3">
        <p className="font-mono text-xs text-slate-400">from {price}</p>
      </div>
    </article>
  );
}

Two things to point out. First, notice how much visual information sits in the classes: colour, spacing, ring, hover state, all in the markup. Second, the Mono font that ships with Tailwind’s font-mono uses the system monospace stack; to pin JetBrains Mono, define it in @theme as shown above.

Utility classes in your markup are not the enemy: they are the trade-off. You give up “clean HTML” and gain “no context switch to a stylesheet”.

# dark mode

Step 4: dark mode toggle in ten lines.

Tailwind 4 supports dark mode via a class or media query. For a working toggle, use the class strategy, add the class to <html>, and prefix any dark variants with dark:.

Enable class-based dark mode in your CSS:

@import 'tailwindcss';

@variant dark (&:where(.dark, .dark *));

Toggle the class from JS on load and on click:

// Read from localStorage or prefers-color-scheme on load.
const prefersDark =
  localStorage.theme === 'dark' ||
  (!('theme' in localStorage) &&
    window.matchMedia('(prefers-color-scheme: dark)').matches);

document.documentElement.classList.toggle('dark', prefersDark);

// Toggle handler for a button in your header.
function toggleTheme() {
  const isDark = document.documentElement.classList.toggle('dark');
  localStorage.theme = isDark ? 'dark' : 'light';
}

Then, in your markup: bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100. The dark: prefix only kicks in when <html class="dark"> is set.

# arbitrary values

Step 5: arbitrary values and @apply.

Tailwind covers most spacing / sizing / colour values out of the box, but every project has a design-system value that does not fit. Arbitrary values use square brackets: [24px], [#5CE1E6], [calc(100vh-64px)].

<div class="mt-[72px] w-[calc(100%-32px)] bg-[#141821] text-[#5CE1E6]">
  <span class="text-[13px] leading-[1.35]">Arbitrary values fit the odd cases.</span>
</div>

For repeated patterns that are truly component-level (a specific card, a specific input), @apply lets you compose utility classes into a real CSS class. Use sparingly: too much @apply defeats the point of Tailwind.

.btn-primary {
  @apply inline-flex items-center gap-2 rounded-md bg-cyan-400 px-4 py-2 font-medium text-slate-900 hover:bg-cyan-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-400;
}

Then in markup: <button class="btn-primary">Read the review</button>.

# plugins

Step 6: the two plugins worth installing on day one.

@tailwindcss/typography: adds the prose class, which styles long-form Markdown / MDX / WYSIWYG content with sensible defaults. Essential on blog posts, docs sites, and any place you have user-generated content.

pnpm add @tailwindcss/typography
@import 'tailwindcss';
@plugin '@tailwindcss/typography';

Then wrap your MDX / rendered HTML in <div class="prose dark:prose-invert"> and you have decent long-form styling for free.

@tailwindcss/forms: resets form controls (input, select, textarea, checkbox, radio) to a consistent base you can then style. If you have ever fought with browser-default checkbox styling, this plugin is the fix.

pnpm add @tailwindcss/forms
@plugin '@tailwindcss/forms';

# migration

Step 7: migrating from vanilla CSS.

If you have an existing site with a hand-written stylesheet, do not try to rewrite everything to Tailwind in one commit. The approach that works:

  • Install Tailwind alongside your existing CSS. Both will load; Tailwind classes take precedence via a light reset.
  • Pick one small component to migrate first (a header, a card, a button). Rewrite the markup with Tailwind classes and delete the corresponding CSS selectors.
  • Migrate the component library first, then pages that use the components inherit the change.
  • Only migrate colour tokens and spacing tokens into @theme once you have a shortlist of the actual values in use across the site.
  • Delete legacy CSS in chunks as pages migrate. Keep a running diff of which selectors are still referenced.

Expect two to four weeks of dual-loading on a mid-sized site (say, 60 templates). If you try to ship it in one PR, the review will be unreadable and something will regress.

# where it breaks

Where Tailwind is not the right call.

Tailwind is not a fit for every project. Skip it if:

  • You are shipping a static marketing page and want minimum bundle size. A hand-written stylesheet of 4 KB will beat a Tailwind bundle after tree-shaking on a page with fifteen unique classes.
  • Your team has a mature design-system CSS methodology (BEM + tokens, CUBE CSS, or plain modular CSS) and the team is happy with it. Migration cost is real.
  • You are working on a codebase with pre-built HTML from a CMS you cannot change (older SharePoint, some CRMs), where you cannot control the class list on rendered markup.

For a working developer team building a React or Vue or Svelte app in 2026, though, Tailwind is almost always worth it. The context-switch cost of jumping to a separate stylesheet is bigger than the “long class list on markup” cost, and it turns out to be easier to review than a growing CSS file that nobody has permission to prune.

# tl;dr

The short version.

Install Tailwind 4 via the Vite plugin (or the standalone CLI). Configure in CSS with @theme. Enable class-based dark mode. Use arbitrary values sparingly, @apply for genuine components, and install the typography and forms plugins on day one. Migrate an existing project component-by-component, not in one PR.

Official docs at tailwindcss.com/docs. For editor plugins that pay off immediately, add the official Tailwind CSS IntelliSense extension to VS Code. More framework tutorials on our tutorials hub and WordPress deep dives.


affiliate disclosure: Web Dev Blog carries affiliate links to Kinsta, WP Engine, Cloudways, DigitalOcean, and Vercel. If you sign up via a link on this site we may receive a commission at no cost to you. This does not influence editorial verdicts, which are based on real tests from a London server. Full policy on /affiliate-disclosure/.