# Override the dark Variant, Don't Configure It

**SHOULD** · **ID:** `design-dark-mode-class` · **Category:** design
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-dark-mode-class

> SHOULD: Use darkMode: "class" for user-controllable themes with light/dark/system options

Redefine the dark variant with @custom-variant so a class can drive the theme, because the darkMode option no longer exists

The `darkMode: 'class'` config option is gone — there is no config file to put it in. In v4 the `dark` variant is just a variant, and you change its behaviour by redefining it with `@custom-variant`. This repository is its own reference implementation: `src/index.css` line 7 reads `@custom-variant dark (&:is(.dark *));`, which is why toggling `.dark` on `<html>` themes this entire app. The reason to override at all is unchanged: the default media-query variant reads the OS setting and nothing else, so a user who wants dark on a machine set to light has no way to ask for it and no way to persist the choice. Driving the variant from a class puts the decision in your hands — read `prefers-color-scheme` as the initial default, then let a toggle write `.dark` and store the preference. Note the selector shape: it must match both the `.dark` element itself and its descendants, otherwise `dark:` utilities on `<html>` never fire.

## Bad — do not do this

`design-dark-mode-class-bad`

```tsx
export function DarkModeClassBad() {
  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="font-medium mb-1">Setting a config option that isn&rsquo;t there</h4>
        <p className="text-xs text-muted-foreground mb-3">
          <code className="font-mono">darkMode</code> was removed in v4 along with the config file.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-error">{`// tailwind.config.js — not read by v4
module.exports = {
  darkMode: 'class',
}`}</pre>
        </div>
      </div>

      <div className="bg-card border border-border rounded-lg p-4 space-y-3">
        <h5 className="text-sm font-medium">So the toggle does nothing</h5>
        <div className="flex gap-2 opacity-50">
          <button className="flex-1 px-3 py-2 bg-muted border border-border rounded text-sm" disabled>
            Light
          </button>
          <button className="flex-1 px-3 py-2 bg-muted border border-border rounded text-sm" disabled>
            Dark
          </button>
        </div>
        <ul className="text-xs space-y-2">
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-error" />
            <span>
              The <code className="font-mono">dark</code> variant still compiles to{' '}
              <code className="font-mono">prefers-color-scheme</code>.
            </span>
          </li>
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-error" />
            <span>
              Adding <code className="font-mono">.dark</code> to <code className="font-mono">&lt;html&gt;</code> changes
              nothing. The theme stays locked to the OS.
            </span>
          </li>
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-error" />
            <span>No error, no warning. The switch just never fires.</span>
          </li>
        </ul>
      </div>

      <p className="text-xs text-error">
        There is no darkMode option to set. The variant itself has to be redefined.
      </p>
    </div>
  );
}
```

## Good — do this

`design-dark-mode-class-good`

```tsx
export function DarkModeClassGood() {
  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="font-medium mb-1">Override the variant in CSS</h4>
        <p className="text-xs text-muted-foreground mb-3">
          This is <code className="font-mono">src/index.css</code> line 7 of this very app.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`@import "tailwindcss";

@custom-variant dark (&:is(.dark *));`}</pre>
        </div>
        <p className="text-xs text-muted-foreground mt-2">
          The selector must match the <code className="font-mono">.dark</code> element and its descendants — otherwise{' '}
          <code className="font-mono">dark:</code> on <code className="font-mono">&lt;html&gt;</code> never fires.
        </p>
      </div>

      <div className="bg-card border border-border rounded-lg p-4 space-y-3">
        <h5 className="text-sm font-medium">Now a class drives the theme</h5>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`document.documentElement.classList
  .toggle('dark', theme === 'dark');`}</pre>
        </div>
        <div className="flex gap-2">
          <button className="flex-1 px-3 py-2 bg-background border border-border rounded text-sm">Light</button>
          <button className="flex-1 px-3 py-2 bg-foreground text-background rounded text-sm">Dark</button>
          <button className="flex-1 px-3 py-2 bg-muted text-muted-foreground rounded text-sm">System</button>
        </div>
        <p className="text-xs text-muted-foreground">
          Seed it from <code className="font-mono">prefers-color-scheme</code>, then let the user override and persist
          the choice.
        </p>
      </div>

      <p className="text-xs text-success">
        The theme toggle at the top of this page works because of exactly this line.
      </p>
    </div>
  );
}
```

## References

- [Tailwind v4: Dark mode](https://tailwindcss.com/docs/dark-mode)
- [Tailwind v4: @custom-variant](https://tailwindcss.com/docs/functions-and-directives)
