How to style an Astro blog with Tailwind v4 and no config file
If you last used Tailwind a couple of years ago, you remember a tailwind.config.js
file where you extended colors, fonts, and spacing in a big JavaScript object.
Tailwind v4 throws that model out. Configuration now lives in your CSS, and
for a lot of projects — including this blog — there’s no config file at all.
Here’s exactly how this Astro blog is set up.
Step 1: The Vite plugin
Astro builds on Vite, and Tailwind v4 ships a first-party Vite plugin. Instead of a PostCSS config and a content-scanning setup, you add one plugin:
// blog/astro.config.mjs
import { defineConfig } from "astro/config"
import tailwindcss from "@tailwindcss/vite"
export default defineConfig({
site: "https://blog.developerehsan.com",
integrations: [/* mdx, sitemap, pagefind... */],
vite: {
plugins: [tailwindcss()],
},
})That’s the entire build-side setup. The @tailwindcss/vite plugin handles
scanning your files for class names and generating CSS on the fly. There’s no
content: [...] array to maintain — v4 figures out what to scan automatically.
Notice there’s no @astrojs/tailwind integration in the integrations
array. In v4 you use the Vite plugin directly rather than an Astro-specific
wrapper.
Step 2: Import Tailwind in your CSS
In v3 you wrote three @tailwind base/components/utilities directives. In v4
it’s a single import at the top of your global stylesheet:
/* blog/src/styles/global.css */
@import "tailwindcss";
@import "@fontsource-variable/jetbrains-mono";That one @import "tailwindcss" pulls in the reset, the utility classes, and
everything else. The second import here just loads the JetBrains Mono variable
font this blog uses — a normal CSS import, nothing Tailwind-specific.
Step 3: Define your design tokens with @theme
This is the big idea in v4. Instead of a JS config object, you declare your
design tokens in a special @theme block. This blog’s entire base palette is
four colors and a font:
/* blog/src/styles/global.css */
@theme {
--font-mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace,
monospace;
--color-bg: oklch(0.05 0 0);
--color-bg-elevated: oklch(0.08 0 0);
--color-fg: oklch(0.92 0 0);
--color-border: oklch(0.2 0 0);
}The magic is in the naming convention. Tailwind reads these CSS variables and generates matching utility classes from them:
--color-bg→ you can now writebg-bg,text-bg,border-bg, etc.--color-bg-elevated→bg-bg-elevated--color-fg→text-fg--color-border→border-border--font-mono→font-mono
So the token is the config. Add a --color-* variable and you instantly have
color utilities for it. There’s no separate “here’s the value” and “here’s the
class name” step — declaring the variable does both.
My portfolio uses the same CSS-first approach — see configuring Tailwind v4 inline with @theme there. A quick note on the color values: these are oklch(...) colors. OKLCH is a
modern color space that’s perceptually uniform, which makes it easy to reason
about lightness — oklch(0.05 0 0) is near-black, oklch(0.92 0 0) is
near-white. The comment at the top of the real file even documents a contrast
floor (nothing informational below oklch(0.6 0 0)) to keep text readable on
the dark background.
flowchart LR A["@theme --color-bg"] --> B[Tailwind reads token] B --> C["utilities: bg-bg, text-bg, border-bg"]
Step 4: Plain CSS still works — and mixes in
Not everything wants to be a utility class. This blog’s terminal aesthetic —
scanlines, glowing text, card chrome — is written as ordinary CSS classes right
alongside the @theme block in the same file:
/* blog/src/styles/global.css */
.terminal-card {
background: oklch(0.08 0 0);
border: 1px solid oklch(0.2 0 0);
transition:
border-color 0.3s,
box-shadow 0.3s,
transform 0.3s;
}
.terminal-card:hover {
border-color: oklch(0.4 0 0);
box-shadow: 0 0 20px oklch(0.92 0 0 / 0.04);
}
.text-glow {
text-shadow:
0 0 24px oklch(0.92 0 0 / 0.5),
0 0 48px oklch(0.92 0 0 / 0.2);
}Because the whole system lives in CSS now, there’s no awkward boundary between “Tailwind stuff” and “my custom CSS.” Utilities and hand-written rules sit in the same file and reference the same tokens.
Why no config file is a good thing
A few concrete wins from the CSS-first approach:
- One source of truth. Your tokens and your custom CSS live together, in the language they’re actually applied in.
- Less indirection. In v3 you’d jump to
tailwind.config.js, add a color, then use it in a class. In v4 you add a--color-*variable and the class exists. Same file, one step. - Real CSS variables. The tokens are genuine CSS custom properties, so you
can also use them in plain CSS (
var(--color-fg)) or override them per-scope. - Less to configure. No content globs, no PostCSS chain — just the Vite plugin and an import.
What to remember
- Tailwind v4 is CSS-first: configure it in your stylesheet, not a JS config file.
- Wire it into Astro with the
@tailwindcss/viteplugin inastro.config.mjs— no@astrojs/tailwindintegration needed. @import "tailwindcss";replaces the old three@tailwinddirectives.- Define tokens in
@themeusing the--color-*/--font-*naming convention, and Tailwind generates matching utilities automatically. - Custom CSS and Tailwind utilities now share one file and one set of tokens.