Tailwind CSS Handbook v4.3

Other Frameworks

Install Tailwind v4 in Laravel, SvelteKit, Astro, Rails, Angular and template-only stacks like Django, Flask and Hugo — plus the recipe that works anywhere.

Tailwind v4 has no framework adapters. Every integration is one of three things: a Vite plugin, a PostCSS plugin, or the standalone CLI. Once you know which one your stack needs, the rest of the setup is the same everywhere.

Which install method#

FrameworkMethodPackageConfig file
LaravelVite plugin@tailwindcss/vitevite.config.ts
SvelteKitVite plugin@tailwindcss/vitevite.config.ts
AstroVite plugin@tailwindcss/viteastro.config.mjs
NuxtVite plugin@tailwindcss/vitenuxt.config.ts
SolidStart, Remix, React RouterVite plugin@tailwindcss/vitevite.config.ts
Next.jsPostCSS@tailwindcss/postcsspostcss.config.mjs
AngularPostCSS@tailwindcss/postcss.postcssrc.json
Ruby on RailsGem or CLItailwindcss-rails or @tailwindcss/clinone
Django, Flask, HugoCLI@tailwindcss/clinone
Plain HTMLCLI@tailwindcss/clinone

Nuxt and Next.js have their own pages — Vue & Nuxt and React & Next.js.

Any framework: the general recipe#

If your framework is not listed anywhere, these three steps are the whole integration.

  1. Install the package that matches your build tool. Vite project → @tailwindcss/vite. PostCSS pipeline → @tailwindcss/postcss. No bundler → @tailwindcss/cli.
  2. Create one CSS file containing @import "tailwindcss"; and put your @theme and custom utilities in it.
  3. Make sure that stylesheet is served, and that Tailwind can see your templates. Source detection starts from the CSS file's directory and walks up to the project root, respecting .gitignore. Add @source only for files it cannot reach.

There is no tailwind.config.js, no content array and no init step.

Laravel#

Laravel ships with Vite, so use the Vite plugin. Add it to the existing plugins array — the Laravel plugin stays where it is.

BASH
npm install tailwindcss @tailwindcss/vite
vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(),
    // …
  ],
})
resources/css/app.css
@import "tailwindcss";

@source "../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php";
@source "../../storage/framework/views/*.php";
@source "../**/*.blade.php";
@source "../**/*.js";
resources/views/layouts/app.blade.php
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    @vite('resources/css/app.css')
  </head>
  <body>
    <h1 class="text-3xl font-bold underline">
      Hello world!
    </h1>
  </body>
</html>
BASH
npm run dev

Why Laravel needs @source lines#

Source detection starts at the stylesheet's directory — resources/css/ — and walks up to the project root, but it skips anything in .gitignore. Laravel gitignores /vendor and /storage, and those directories contain classes you ship:

  • vendor/laravel/.../Pagination/ — the framework's own paginator views are styled with Tailwind. Without this line, pagination links render unstyled.
  • storage/framework/views/ — compiled Blade. Cheap insurance for classes assembled by directives.
  • Your own resources/ tree — the last two lines name your Blade templates and JS explicitly, so the paths above do not narrow detection to vendor/ and storage/ alone.

Add another @source for any package that ships Tailwind-styled Blade views into vendor/.

SvelteKit#

BASH
npm install tailwindcss @tailwindcss/vite
vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [
    tailwindcss(),
    sveltekit(),
  ],
});
src/app.css
@import "tailwindcss";

Import it from the root layout so every route inherits it:

src/routes/+layout.svelte
<script>
  let { children } = $props();
  import "../app.css";
</script>

{@render children()}

Templates live in src/routes/ and src/lib/, both under the project root, so they are scanned automatically.

@apply in a Svelte <style> block#

A <style> block in a .svelte file compiles as its own stylesheet, with no access to your theme — the same constraint as a Vue SFC. Add @reference to bring the theme into scope:

src/routes/+page.svelte
<h1 class="text-3xl font-bold underline">
  Hello world!
</h1>

<style lang="postcss">
  @reference "tailwindcss";
  :global(html) {
    background-color: theme(--color-gray-100);
  }
</style>

Reference your own src/app.css instead of the package when your theme is customised. @reference reads the theme without emitting anything, so nothing is duplicated in the output — the mechanics are covered in Vue & Nuxt.

Astro#

Astro is Vite-based, and exposes Vite's config under a vite key.

BASH
npm install tailwindcss @tailwindcss/vite
astro.config.mjs
// @ts-check
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite";

// https://astro.build/config
export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});
src/styles/global.css
@import "tailwindcss";

Import the stylesheet from the component frontmatter:

src/pages/index.astro
---
import "../styles/global.css";
---

<h1 class="text-3xl font-bold underline">
  Hello world!
</h1>

In a real site, put that import in a shared layout under src/layouts/ rather than in every page. Templates live in src/pages/, src/layouts/ and src/components/ — all scanned automatically, including any .jsx, .vue or .svelte island components alongside them.

Ruby on Rails#

Two options. The gem is the official guide's answer and manages the build for you; the CLI is the escape hatch when you want the same setup as every other non-bundled project.

With tailwindcss-rails#

BASH
bundle add tailwindcss-rails
./bin/rails tailwindcss:install
BASH
./bin/dev

The installer creates the input stylesheet, wires the layout to the compiled output, and adds a watch process to Procfile.dev — which is what ./bin/dev runs.

PathRole
app/assets/tailwind/application.cssYour input file — @import "tailwindcss";, @theme, custom utilities
app/assets/builds/tailwind.cssGenerated output, served by the asset pipeline

Two rake tasks drive it directly if you are not using ./bin/dev:

BASH
./bin/rails tailwindcss:build    # one-off, for deploys
./bin/rails tailwindcss:watch    # rebuild on change

Templates live in app/views/, plus app/helpers/ and app/components/ if you use ViewComponent — all under the project root and picked up automatically.

With @tailwindcss/cli#

If you would rather not add a gem, the CLI is identical to any other Node-free-ish setup:

BASH
npm install tailwindcss @tailwindcss/cli
app/assets/stylesheets/application.css
@import "tailwindcss";
BASH
npx @tailwindcss/cli -i ./app/assets/stylesheets/application.css -o ./app/assets/builds/tailwind.css --watch

Add the watch command to Procfile.dev alongside rails server, and use --minify instead of --watch in your deploy task.

Django, Flask and Hugo#

None of these have a JavaScript build step, so run the CLI beside the dev server. The pattern is the same in all three — only the paths change.

BASH
npm install tailwindcss @tailwindcss/cli
src/input.css
@import "tailwindcss";
package.json
{
  "scripts": {
    "dev": "npx @tailwindcss/cli -i ./src/input.css -o ./static/css/site.css --watch",
    "build": "npx @tailwindcss/cli -i ./src/input.css -o ./static/css/site.css --minify"
  }
}

Run npm run dev in one terminal and your framework's server in another.

Django#

Templates live in templates/ at the project root and in each app's <app>/templates/ directory. Both are under the project root, so source detection finds them with no configuration. Output goes into a static/ directory listed in STATICFILES_DIRS:

templates/base.html
{% load static %}
<link rel="stylesheet" href="{% static 'css/site.css' %}">

If your CSS input file lives inside an app directory, @source the project-level template folder explicitly:

CSS
@source "../../templates";

Flask#

Templates are in templates/, static assets in static/:

templates/base.html
<link rel="stylesheet" href="{{ url_for('static', filename='css/site.css') }}">

Hugo#

Layouts live in layouts/, content in content/. Write the compiled CSS into static/css/ so Hugo copies it verbatim:

layouts/_default/baseof.html
<link rel="stylesheet" href="{{ "css/site.css" | relURL }}">

Plain HTML#

No framework, no bundler, no config file.

BASH
npm install tailwindcss @tailwindcss/cli
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch
index.html
<link rel="stylesheet" href="/dist/output.css">
<h1 class="text-3xl font-bold underline">Hello world!</h1>

Angular#

Angular's build runs PostCSS, so it uses @tailwindcss/postcss — with a .postcssrc.json rather than the postcss.config.mjs other PostCSS setups use.

BASH
npm install tailwindcss @tailwindcss/postcss postcss --force
.postcssrc.json
{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}
src/styles.css
@import "tailwindcss";
BASH
ng serve

src/styles.css is already listed in the styles array in angular.json, so no wiring is needed. Templates live in src/app/ as .html files or inline template strings — both are scanned, since detection reads files as text.

Two Angular specifics:

  • --force is part of the official install command. Angular's dependency tree can otherwise refuse the install over a peer range.
  • [ngClass] and [class] bindings follow the same rule as everywhere else. [ngClass]="'bg-' + colour + '-500'" produces no CSS; map component state to complete class strings instead. The IntelliSense extension already treats ngClass as a class attribute.

Common mistakes#

  • Following a guide that opens with an init command. v4 has no config file to generate, so that step was removed. Everything after it in that guide is v3 too.
  • Using the bare tailwindcss PostCSS plugin name. plugins: { tailwindcss: {} } throws in v4; it must be @tailwindcss/postcss.
  • Skipping Laravel's @source lines. vendor/ and storage/ are gitignored, so paginator views and compiled Blade are invisible to source detection and render unstyled.
  • Keeping a v3-era framework integration. @astrojs/tailwind and the v3 Nuxt module inject @tailwind directives and a tailwind.config.js. Remove them before installing the v4 plugin.
  • Installing @tailwindcss/vite in a non-Vite project. Angular, Rails and Django have no Vite pipeline; the plugin is loaded and silently never runs.
  • Pointing the CLI's -o at a path the server does not serve. The build succeeds and writes a perfectly good file that nothing ever requests.
  • Forgetting --minify in the production script. The --watch command is for development; deploys need the minified build.
  • Building class names in template syntax. Blade's {{ }}, Django's {% %}, Hugo's {{ }} and Angular's [ngClass] are all evaluated long after Tailwind has finished scanning.