Style Dark Text with Tailwind CSS v4 and v4.2

Master dark mode text in Tailwind CSS v4 and v4.2

by Yucel F. Sahan
6 min read
Updated on

Style dark text the right way with v4's CSS-first approach

Dark Mode Text in Tailwind CSS v4 - v4.2

Dark mode has gone from a nice-to-have to a baseline expectation. Users demand it, OLED screens benefit from it, and accessibility standards increasingly assume you've thought about it. The good news: Tailwind CSS v4 makes implementing dark mode text styling cleaner than ever — but the approach has changed significantly since v3. If your project still references tailwind.config.js for dark mode, this guide is for you.

What changed about dark mode in Tailwind v4?

There is no more tailwind.config.js for dark mode. Tailwind v4 is fully CSS-first. The old darkMode: 'class' or darkMode: 'media' config keys are gone. Everything now lives in your CSS file using @custom-variant.

In v3, you'd write:

// tailwind.config.js (v3 — do not use in v4)
module.exports = {
  darkMode: 'class',
}

In v4, you write this in your CSS instead:

/* app.css */
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

That one line replaces the entire config key. The dark: prefix in your HTML works exactly the same as before — only the configuration method changed.

How does the default dark mode work in Tailwind v4?

By default, Tailwind v4 uses the user's OS preference via prefers-color-scheme. No configuration required. The dark: variant is active automatically when the system is set to dark mode.

<h1 class="text-gray-900 dark:text-gray-100">
  This heading adapts to your system theme
</h1>

The text-gray-900 applies in light mode. When the OS is in dark mode, dark:text-gray-100 takes over. No JavaScript, no class toggling, no setup needed.

This is the simplest path — ideal for content sites, documentation, and blogs where you don't need a user-controlled toggle.

How do you enable a manual dark mode toggle in Tailwind v4?

Override the dark variant in your CSS using @custom-variant, then toggle the .dark class on <html> with JavaScript.

Step 1 — declare the variant in your CSS:

@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

Step 2 — add the dark class to your HTML element:

<html class="dark">
  <body>
    <p class="text-gray-900 dark:text-white">Dark mode is active</p>
  </body>
</html>

Step 3 — wire up a toggle with JavaScript:

// Inline this in <head> to avoid flash of wrong theme (FOUC)
document.documentElement.classList.toggle(
  "dark",
  localStorage.theme === "dark" ||
    (!("theme" in localStorage) &&
      window.matchMedia("(prefers-color-scheme: dark)").matches)
);

// User explicitly chooses dark
localStorage.theme = "dark";

// User explicitly chooses light
localStorage.theme = "light";

// User resets to system default
localStorage.removeItem("theme");

This pattern supports three states: light, dark, and system — exactly what users expect from a modern theme toggle.

Can you use a data attribute instead of a class for dark mode?

Yes. Use @custom-variant with an attribute selector instead of a class selector.

@import "tailwindcss";
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));

Then activate dark mode by setting the attribute on your root element:

<html data-theme="dark">
  <body>
    <div class="bg-white dark:bg-black">
      <!-- Dark mode styles apply here -->
    </div>
  </body>
</html>

This is useful when you're managing multiple themes or integrating with a design system that uses data attributes for theming instead of class names.

How do you style dark text with Tailwind's dark: prefix?

Prefix any text color utility with dark: to define its dark mode value.

<p class="text-gray-900 dark:text-gray-100">Body copy</p>
<h2 class="text-gray-800 dark:text-white">Section heading</h2>
<a href="#" class="text-blue-600 dark:text-blue-400 hover:underline">Link</a>

The pattern is always: default (light mode) class first, dark: variant second. Tailwind generates the correct CSS at build time — there's no runtime cost.

You can combine dark: with responsive and state variants too:

<a class="text-gray-900 dark:text-gray-100 hover:text-blue-600 dark:hover:text-blue-300">
  Hover changes color in both modes
</a>

How does dark mode work with Tailwind Typography (prose)?

Use dark:prose-invert on any element that uses the prose class.

<article class="prose dark:prose-invert">
  <h1>Article Title</h1>
  <p>Content automatically adjusts colors for dark mode.</p>
</article>

prose-invert flips the entire typography palette — headings, body text, links, code blocks, and blockquotes — so you don't have to override each one manually.

Best practices for dark text contrast and accessibility

Aim for a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text, per WCAG 2.1 AA.

A few practical rules:

Pair text-gray-900 with dark:text-gray-100 for body text — this is the most common and reliable combination. Avoid pure white (text-white) on pure black backgrounds for long reading; the contrast is too harsh and causes eye strain. Use text-gray-300 or text-gray-200 as your dark mode default for secondary copy. Always check your palette with the WebAIM Contrast Checker before shipping.

For interactive elements, make sure both the default and hover states pass contrast in both modes:

<button class="text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400">
  Submit
</button>

Using CSS variables with dark mode in Tailwind v4

Define theme tokens in @theme and override them per-mode in @layer base.

Tailwind v4's CSS-first system pairs naturally with CSS custom properties:

@import "tailwindcss";

@theme {
  --color-text-primary: var(--color-gray-900);
  --color-text-secondary: var(--color-gray-600);
}

@layer base {
  .dark {
    --color-text-primary: var(--color-gray-100);
    --color-text-secondary: var(--color-gray-400);
  }
}

Then reference them in your markup using Tailwind's arbitrary value syntax or via extended theme utilities. This approach is ideal for design systems where tokens need to be consumed outside of Tailwind (e.g., in Figma or Storybook).

Real-world example: blog post card with dark text

<article class="bg-white dark:bg-gray-900 rounded-xl p-6 shadow ring ring-gray-900/5">
  <h2 class="text-xl font-bold text-gray-900 dark:text-white">
    Getting Started with Tailwind v4
  </h2>
  <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
    A practical guide to the new CSS-first configuration system.
  </p>
  <a href="#" class="mt-4 inline-block text-blue-600 dark:text-blue-400 hover:underline text-sm font-medium">
    Read more
  </a>
</article>

No configuration file, no darkMode key, no plugin. The CSS import + @custom-variant line handles everything.


Summary: Tailwind v4 vs v3 dark mode at a glance

Feature

v3

v4 / v4.2

Config location

tailwind.config.js

app.css via @custom-variant

Media strategy

darkMode: 'media'

Default (no setup needed)

Class strategy

darkMode: 'class'

@custom-variant dark (&:where(.dark, .dark *))

Data attribute

Not built-in

@custom-variant dark (&:where([data-theme=dark], ...))

dark: prefix in HTML

Same

Same

JIT / config purge

Required

Automatic (CSS-first)

The dark: prefix in your markup hasn't changed at all. What changed is where and how you configure the strategy — and v4's approach is strictly cleaner.

FAQ

Does Tailwind v4 still use tailwind.config.js for dark mode?

No. Tailwind v4 is CSS-first. You configure dark mode with @custom-variant directly in your CSS file — the darkMode config key no longer exists.

Do I need JavaScript to enable dark mode in Tailwind v4?

Not if you rely on the user's OS preference. The dark: variant works automatically via prefers-color-scheme with zero JavaScript. You only need JS if you want a manual toggle.

Can I use dark: with hover and responsive variants at the same time?

Yes. Stack them in this order: responsive prefix first, then dark:, then state variant. For example: md:dark:hover:text-white works as expected.

Yucel F. Sahan

Yucel is a digital product creator and content writer with a knack for full-stack development. He loves blending technical know-how with engaging storytelling to build practical, user-friendly solutions. When he's not coding or writing, you'll likely find him exploring new tech trends or getting inspired by nature.