TypeScript

Fix Tailwind v4: dark class changes no background colour

Keep one bg-bgContrast class while changing its inherited colour, with real browser checks for the wrong theme syntax and the inline-token trap.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

Define the colour token once in @theme, then change its value in an ordinary .dark CSS rule inside the theme layer. The generated background utility continues to read the variable, so its existing unprefixed class can follow the new value. Keep the custom dark variant if the application also uses dark-prefixed utilities, but do not expect that directive to assign alternate token values.

The reported migration problem wanted a single bg-bgContrast class to change when the dark class was added to the HTML root. The source already defined a custom dark variant, then tried this token declaration:

css
@theme {
  --color-bgContrast: #f9f9f9;
}
 
@theme dark: {
  --color-bgContrast: #66C8B6;
}

The second block is not a dark-selector override. In the current compiler reproduction, it did not even fail the build: it replaced the global token value, leaving the plain background the same before and after toggling the class. Diagnose the selector and computed variable rather than assuming that a successful build proves the theme switches.

Why it happens#

Tailwind's theme-variable documentation requires theme declarations at the top level. Attaching dark: to that directive does not turn its declarations into a matching rule for the root's dark class.

The compiler source for the tested release parses theme options such as inline and reference. Its option parser does not make the source's dark: text a dark variant. The browser test confirmed the practical result for this stylesheet: the later mint colour became the root value in both states.

The utility itself was generated correctly. Its background declaration read the colour variable. What was missing was a conditional CSS assignment to that variable. This differs from a missing Tailwind installation or an absent generated utility.

Fix#

1. Replace the second theme block with a selector rule#

For the source's root-class approach, use this stylesheet:

css
@import 'tailwindcss';
 
@custom-variant dark (&:where(.dark, .dark *));
 
@theme {
  --color-bgContrast: #f9f9f9;
}
 
@layer theme {
  .dark {
    --color-bgContrast: #66C8B6;
  }
}

The repair changes only the unsupported second declaration block into a real selector-scoped override. The import, token name, custom variant and component class remain the same. Keep the application's other CSS and plugin declarations; the small reproduction isolates the colour mechanism rather than rebuilding a full Vite application.

CSS custom properties participate in the cascade and normally inherit, as MDN's custom-property reference explains. With the marker on the root, the element using the token inherits its alternate value. No second background class is required for this specific variable-backed colour.

2. Toggle the selector that the stylesheet actually uses#

A minimal client-side toggle and its unchanged background class are:

html
<button id="theme-toggle" class="bg-bgContrast">Toggle theme</button>
 
<script>
  document.getElementById('theme-toggle').addEventListener('click', () => {
    document.documentElement.classList.toggle('dark');
  });
</script>

The relevant change is on the document root, rather than replacing the button's class. In browser DevTools, confirm that the root really receives dark and that the button retains bg-bgContrast. An application using a different marker, such as a data attribute, needs a corresponding token selector as well as its variant selector. Defining one does not automatically rewrite the other.

This toggle does not persist a preference or choose a system default. Those are separate behaviours. The Tailwind dark-mode guide documents selector-driven and system-driven approaches; the example here deliberately follows a root class.

3. Inspect the generated utility and loaded stylesheet#

Rebuild through the application's existing Tailwind integration. The reproduced output included:

css
.bg-bgContrast {
  background-color: var(--color-bgContrast);
}

If compilation stops with the PostCSS-plugin message instead, the Tailwind v4 PostCSS integration fix addresses that separate build failure. Here, the rule already exists and the failing behaviour is the unchanged computed colour.

Verify the fix#

The stylesheet was compiled and exercised in Chromium on 13 October 2026, using Tailwind CSS and its CLI at 4.3.3, Playwright 1.63.0 and Chromium 153.0.8010.12. The isolated container used the existing Playwright image and locally compiled CSS; it did not load the browser CDN compiler or run a Vite project.

The browser's computed backgrounds for the plain button were:

StylesheetNo root markerRoot has dark
Original second theme blockrgb(102, 200, 182)rgb(102, 200, 182)
Selector overridergb(249, 249, 249)rgb(102, 200, 182)

The component class was unchanged in every measurement. The test recorded the root's token value separately from the element's computed background, and asserted the expected results. The single-block repair changed the browser behaviour without adding a dark-prefixed background class.

To inspect those same layers in your own page, run:

javascript
const root = document.documentElement;
const button = document.getElementById('theme-toggle');
 
console.log(root.className);
console.log(getComputedStyle(root).getPropertyValue('--color-bgContrast'));
console.log(getComputedStyle(button).backgroundColor);

A marker change without a token change points at the selector or a competing token declaration. A token change without a background change points at the generated background rule or another winning background declaration. Look at the actual computed property and the Styles panel instead of replacing every component class as a first response.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

The custom variant and the token override are separate controls#

The custom-variant directive changes the condition applied to dark-prefixed utilities. It does not assign the mint colour to a plain token. In a negative control, removing that directive from the repaired stylesheet left the unprefixed button switching correctly: the ordinary .dark variable rule still applied.

A second button used bg-bgContrast and dark:bg-black. With the custom variant present, adding the root marker made that button black. Removing the custom variant restored Tailwind's default system-preference condition for the prefixed utility, while the plain token continued to follow the class rule.

Another control set the browser's preferred colour scheme to dark without adding the root class. With the source's custom class variant still present, neither test button switched. That is expected for this configuration; it is not proof that Tailwind's system-based dark mode is broken.

Inline can disconnect a literal token from its override#

Do not add inline to this direct-colour theme block as an automatic dark-mode fix. The theme documentation's variable-reference section explains that inline makes utilities use the theme variable's value directly rather than referencing that theme variable.

The control changed only the repaired theme block to inline while retaining its literal light colour. The output became:

css
.bg-bgContrast {
  background-color: #f9f9f9;
}

The root token still changed to the mint value when dark was added, but the button stayed light. Its rule now contained a fixed colour, so changing the unused token could not change that background.

For a shadcn colour alias or an absent border-border utility, use the separate border-token migration guide. The control here concerns a directly declared literal colour whose utility already exists; it does not establish a universal rule for variable aliases.

Reuse the variant selector for a root token rule#

If the application intentionally centralises its dark selector in the custom variant, the source's second answer provides another option. Replace the direct .dark rule with an ordinary root rule that applies the variant:

css
@layer theme {
  :root {
    @variant dark {
      --color-bgContrast: #66C8B6;
    }
  }
}

This uses the variant directive inside a CSS rule, not inside a conditional theme declaration. In the browser control, it produced the same root-class colour switch. Changing the custom selector and root marker to night also worked without changing this token rule.

That control applies to a root marker, as the source requested. Select the appropriate scope before adapting it to a nested themed subtree. The simpler direct .dark override is sufficient when the application's marker is fixed and the utility keeps referencing the token.

Missing editor suggestions have their own Tailwind v4 IntelliSense entrypoint fix. For this browser failure, verify the full chain: root marker, consumed variable, computed background. The component should keep its original class while that background changes.

Further reading: Fix Tailwind v4 padding and margin not working.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote