14

My tailwind.config.js in v3 looks like this, but I can't find a way to use it in v4:

theme: {
  extend: {
    colors: {
      lightHover: '#fcf4ff',
      darkHover: '#2a004a',
      darktheme: '#11001f',
    },
    fontFamily: {
      Outfit: ["Outfit", "sans-serif"],
      Ovo: ["Ovo", "serif"]
    },
    boxShadow: {
      'black': '4px 4px 0 #000',
      'white': '4px 4px 0 #fff',
    },
    gridTemplateColumns: {
      'auto': 'repeat(auto-fit, minmax(200px, 1fr))'
    }
  },
},
darkMode: 'selector',

This is a piece of code for v3,and I can customize the color of the dark theme instead of using black like this:

<div className="dark:bg-darktheme"></div>

But how can I do the same thing in v4? Does anyone have the same problem? Just write bg-black?

Can't find detailed tailwind.config.js documentation.

1
  • Starting from TailwindCSS v4, a CSS-first configuration is preferred. Check my answer to see what this means. I also explain how you can still use tailwind.config.js in v4. Commented Mar 11 at 6:57

3 Answers 3

28

Starting from TailwindCSS v4, the CSS-first configuration is preferred, meaning the init process and the tailwind.config.js file have been removed.

Customize theme with CSS-first directives

In a CSS-first configuration, several directives are available. For theme customization, you can primarily use the @theme directive:

@import "tailwindcss";

@theme {
  --font-display: "Satoshi", "sans-serif";
  --breakpoint-3xl: 120rem;
  --color-avocado-100: oklch(0.99 0 0);
  --color-avocado-200: oklch(0.98 0.04 113.22);
  --color-avocado-300: oklch(0.94 0.11 115.03);
  --color-avocado-400: oklch(0.92 0.19 114.08);
  --color-avocado-500: oklch(0.84 0.18 117.33);
  --color-avocado-600: oklch(0.53 0.12 118.34);
  --ease-fluid: cubic-bezier(0.3, 0, 0, 1);
  --ease-snappy: cubic-bezier(0.2, 0, 0, 1);
  /* ... */
}

Theme variables are defined in namespaces and each namespace corresponds to one or more utility class or variant APIs.

Set dark directive with CSS-first

Starting from v4, a new @custom-variant directive is introduced, allowing you to customize the behavior of dark: or create your own custom variants, such as coffee:.

document.querySelector('button').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
});
<script src="https://unpkg.com/@tailwindcss/browser"></script>
<style type="text/tailwindcss">
/* changed the behavior of dark: (default: based on prefers-color-scheme) to work based on the presence of the .dark parent class */
@custom-variant dark (&:where(.dark, .dark *));

@theme {
  --color-pink: #eb6bd8;
}

@layer theme {
  :root, :host {
    @variant dark {
      --color-pink: #8e0d7a;
    }
  }
}
</style>

<button class="size-20 bg-pink dark:text-white">Click Here</button>
<div class="w-50 h-12 bg-purple-200 dark:bg-purple-900 dark:text-white">
  Lorem Ipsum
</div>

Additionally, without JS, it's possible to detect whether the system prefers light or dark mode by default, and integrate this into the dark: variant behavior. For this, your light/dark toggle needs to support three states: system/light/dark. You'll also need an extra .system class, but the dark: variant can work the same way with both .system and .dark classes:

How to declare dark or more themes in TailwindCSS v4

Following the dark example, an unlimited number of variants and themes can be declared.

document.querySelector('#toggle-dark').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
  document.documentElement.classList.remove('coffee');
});

document.querySelector('#toggle-coffee').addEventListener('click', () => {
  document.documentElement.classList.toggle('coffee');
  document.documentElement.classList.remove('dark');
});
<script src="https://unpkg.com/@tailwindcss/browser"></script>
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant coffee (&:where(.coffee, .coffee *));

@theme {
  --color-pink: #eb6bd8;
  --color-tsunami: #77b4ea;
}

@layer theme {
  :root, :host {
    @variant dark {
      --color-pink: #8e0d7a;
      --color-tsunami: #0d84ec;
    }
    @variant coffee {
      --color-pink: #a67ca8;
      --color-tsunami: #b57913;
    }
  }
}
</style>

<div class="mb-4">
  <button id="toggle-dark" class="px-4 py-2 bg-sky-600 hover:bg-sky-950 text-white cursor-pointer rounded-lg">Toggle Dark</button>
  <button id="toggle-coffee" class="px-4 py-2 bg-amber-600 hover:bg-amber-950 text-white  cursor-pointer rounded-lg">Toggle Coffee</button>
</div>

<button class="size-20 bg-pink dark:text-white coffee:text-amber-50">Hello World</button>
<div class="w-50 h-12 bg-tsunami dark:text-white coffee:text-orange-200">
  Lorem Ipsum
</div>

References:

Disable dark mode, use light: instead of dark::

Sign up to request clarification or add additional context in comments.

1 Comment

I needed to ask a clarification (about dynamically switched themes), but it was too bulky as a comment. So I posted a new question: stackoverflow.com/questions/79620901/…. Would you be kind enough to take a look!
3

light-dark() CSS function

With light-dark(), a color can be tied to the color-scheme. Based on the color-scheme, the browser selects the color corresponding to the current scheme. This essentially simplifies the declaration of light and dark themes, allowing it to be done in a single place, within @theme.

@theme {
  --color-pink: light-dark(#eb6bd8, #8e0d7a);
  --color-tsunami: light-dark(#77b4ea, #0d84ec);
}

Attention: TailwindCSS is shipped under the hood with LightningCSS. While LightningCSS does provide a fallback for older browsers to replace light-dark(), TailwindCSS does not apply it due to an open issue, meaning it is not available. TailwindCSS officially guarantees support starting from Baseline 2023, but light-dark() is a CSS function that was only stably introduced in Baseline 2024. This means that if you use it, your minimum browser version requirements will rise to the level where light-dark() was introduced. More details:

document.querySelector('button').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
});
<script src="https://unpkg.com/@tailwindcss/browser"></script>
<style type="text/tailwindcss">
/* changed the behavior of dark: (default: based on prefers-color-scheme) to work based on the presence of the .dark parent class */
@custom-variant dark (&:where(.dark, .dark *));

/* WARNING: This way, instead of Chrome 111, at least Chrome 123 will be required, and instead of Safari 16.4, Safari 17.5 will be needed for it to work. */
@theme {
  --color-pink: light-dark(#eb6bd8, #8e0d7a);
  --color-tsunami: light-dark(#77b4ea, #0d84ec);
}

* {
  color-scheme: light; /* apply "light" (first) color from light-dark() */
  
  @variant dark {
    color-scheme: dark; /* apply "dark" (second) color from light-dark() */
  }
}
</style>

<button class="size-20 bg-pink dark:text-white">Click Here</button>
<div class="w-50 h-12 bg-tsunami dark:text-white">
  Lorem Ipsum
</div>

var(--tw-light, ...) var(--tw-dark, ...) alternative

The previously mentioned question has an answer that explains how the behavior of light-dark() can be replicated with a little extra work. This way, you can avoid raising the minimum browser version requirement in your project.

@theme inline {
  --color-pink: var(--tw-light, #eb6bd8) var(--tw-dark, #8e0d7a);
  --color-tsunami: var(--tw-light, #77b4ea) var(--tw-dark, #0d84ec);
}

Note: Since @theme ships the given value into a global variable, the value cannot be a variable itself; otherwise, CSS cannot properly track the fallback values. Therefore, you should always use @theme inline. @theme inline does not embed the values into a global variable. However, it isn't necessary, since later on we don't want to override the color for other themes, as each theme can be declared locally in a single line.

document.querySelector('button').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
});
<script src="https://unpkg.com/@tailwindcss/browser"></script>
<style type="text/tailwindcss">
/* changed the behavior of dark: (default: based on prefers-color-scheme) to work based on the presence of the .dark parent class */
@custom-variant dark (&:where(.dark, .dark *));

/* NOTE: It still works starting from Chrome 111 and Safari 16.4. */
@theme inline {
  --color-pink: var(--tw-light, #eb6bd8) var(--tw-dark, #8e0d7a);
  --color-tsunami: var(--tw-light, #77b4ea) var(--tw-dark, #0d84ec);
}

* {
  color-scheme: light;
  --tw-light: initial;
  --tw-dark: ;
  
  @variant dark {
    color-scheme: dark;
    --tw-light: ;
    --tw-dark: initial;
  }
}
</style>

<button class="size-20 bg-pink dark:text-white">Click Here</button>
<div class="w-50 h-12 bg-tsunami dark:text-white">
  Lorem Ipsum
</div>

2 Comments

If you use the second "var(--tw-light, ...) var(--tw-dark, ...) alternative" option, please give feedback on whether it works fully as expected.
You can inject multiple variables, allowing us to cover several templates: How to use custom color themes in TailwindCSS v4
0

Building on the @rozsazoltan's answer titled "var(--tw-light, ...) var(--tw-dark, ...) alternative", you can inject multiple variables, allowing us to cover several templates.

@theme inline {
  --color-pink: var(--tw-light, #eb6bd8) var(--tw-dark, #8e0d7a) var(--tw-coffee, #a67ca8);
  --color-tsunami: var(--tw-light, #77b4ea) var(--tw-dark, #0d84ec) var(--tw-coffee, #b57913);
}

document.querySelector('#toggle-dark').addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
  document.documentElement.classList.remove('coffee');
});

document.querySelector('#toggle-coffee').addEventListener('click', () => {
  document.documentElement.classList.toggle('coffee');
  document.documentElement.classList.remove('dark');
});
<script src="https://unpkg.com/@tailwindcss/browser"></script>
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant coffee (&:where(.coffee, .coffee *));

/* NOTE: It still works starting from Chrome 111 and Safari 16.4. */
@theme inline {
  --color-pink:
    var(--tw-light, #eb6bd8)
    var(--tw-dark, #8e0d7a)
    var(--tw-coffee, #a67ca8);
  --color-tsunami:
    var(--tw-light, #77b4ea)
    var(--tw-dark, #0d84ec)
    var(--tw-coffee, #b57913);
}

* {
  color-scheme: light;
  --tw-light: initial;
  --tw-dark: ;
  --tw-coffee: ;
  
  @variant dark {
    color-scheme: dark;
    --tw-light: ;
    --tw-dark: initial;
    --tw-coffee: ;
  }
  
  @variant coffee {
    /* You can keep the value on "light" or "dark" as needed. */
    /* color-scheme: coffee; is invalid. */
    /* https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme#syntax */
    color-scheme: light;
    --tw-light: ;
    --tw-dark: ;
    --tw-coffee: initial;
  }
}
</style>

<div class="mb-4">
  <button id="toggle-dark" class="px-4 py-2 bg-sky-600 hover:bg-sky-950 text-white cursor-pointer rounded-lg">Toggle Dark</button>
  <button id="toggle-coffee" class="px-4 py-2 bg-amber-600 hover:bg-amber-950 text-white  cursor-pointer rounded-lg">Toggle Coffee</button>
</div>

<button class="size-20 bg-pink dark:text-white coffee:text-amber-50">Hello World</button>
<div class="w-50 h-12 bg-tsunami dark:text-white coffee:text-orange-200">
  Lorem Ipsum
</div>

Note: Since @theme ships the given value into a global variable, the value cannot be a variable itself; otherwise, CSS cannot properly track the fallback values. Therefore, you should always use @theme inline. @theme inline does not embed the values into a global variable. However, it isn't necessary, since later on we don't want to override the color for other themes, as each theme can be declared locally in a single line.

Note: I created this answer separately because it differs on its own from the alternative given for light-dark() (what can't handle more themes), and as an extension of that alternative, I think it's better to keep it tracked independently.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.