1

I have an application getting migrated from Tailwindcss v3 to v4.1.

I make use of color themes, dynamically switched by the client using data attribute or class on the <body>. Thus elements can simply be styled using var(--primary-500), or even bg-neutral-100 text-primary-500 border-primary-500, while the actual color palette is determined dynamically.

Here's an outline of the theme system:

//color-schemes.js - which is imported into the tailwind.config.js
module.exports = {
  blue: {
    primary: {
      50: "#f1f9ff",
      100: "#d9efff",
      200: "#b1dfff",
      
    },
    accent: {
      50: "#fff6ed",
      100: "#ffedd5",
      200: "#fed7aa",
    
    },
    neutral: {
      50: "#f8fafc",
      100: "#f1f5f9",
      200: "#e2e8f0",
      
    },
  },
  green: {
    primary: {
      50: "#f6faf3",
      100: "#e3f1dc",
    
    },
    accent: {
      50: "#ecfdf5",
      100: "#d1fae5",
    },
  }
...etc

//blue.css
[data-theme="blue"] {
  --primary-50: theme("colors.blue.primary.50");
  --primary-100: theme("colors.blue.primary.100");
  --primary-200: theme("colors.blue.primary.200");

  --secondary-50: theme("colors.blue.secondary.50");
  --secondary-100: theme("colors.blue.secondary.100");
  --secondary-200: theme("colors.blue.secondary.200");
}
  
//green.css
[data-theme="green"] {
  --primary-50: theme("colors.green.primary.50");
  --primary-100: theme("colors.green.primary.100");
  --primary-200: theme("colors.green.primary.200");

  --secondary-50: theme("colors.green.secondary.50");
  --secondary-100: theme("colors.green.secondary.100");
  --secondary-200: theme("colors.green.secondary.200");
}

I'm now unable to get this to work in v4 as config.js is discouraged. What is the css-first way to achieve this?

1
  • For v3, I often used the tw-colors plugin. I haven't had a chance to review how it's been adapted for v4 yet. Although the beginning of the README strongly suggests that I should forget about it starting from v4. Which makes sense, since you'll see further down that you have to write about the same amount and in a similar way using CSS-first in v4 as you did with the tw-colors plugin in v3. Commented May 14 at 8:32

1 Answer 1

2

First and foremost, you need to declare the color for TailwindCSS in the @theme, regardless of where it will be used. Once that's done, in some cases you can override the color.

/* declare primary-50 ... primary-900 etc. */
@theme {
  --color-primary-50: #111;
  --color-primary-900: #999;
} 

After that, you can override the colors inside the @layer theme like this:

/* overwrite default primary values */
@layer theme {
  [data-theme="blue"] {
    --color-primary-50: #f1f9ff;
    --color-primary-900: #3f88c4;
  }

  [data-theme="green"] {
    --color-primary-50: #f6faf3;
    --color-primary-900: #1d843b;
  }
}

Example with global variables:

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";

:root {
  --default-primary-50: #d8ade5;
  --default-primary-900: #ae1fde;
  --blue-primary-50: #f1f9ff;
  --blue-primary-900: #3f88c4;
  --green-primary-50: #f6faf3;
  --green-primary-900: #1d843b;
}

@theme {
  --color-primary-50: var(--default-primary-50);
  --color-primary-900: var(--default-primary-900);
}

@layer theme {
  [data-theme="blue"] {
    --color-primary-50: var(--blue-primary-50);
    --color-primary-900: var(--blue-primary-900);
  }

  [data-theme="green"] {
    --color-primary-50: var(--green-primary-50);
    --color-primary-900: var(--green-primary-900);
  }
}

button {
  @apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>

<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>

<div class="mt-4 p-4 bg-primary-50 text-primary-900 font-bold">
  Hello World
</div>

I worked with three themes: default (when there is no data-theme), and blue and green (when the corresponding data-theme is set).

You can optionally declare custom variants for the data-themes:

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";

@custom-variant blue (&:where([data-theme=blue], [data-theme=blue] *));
@custom-variant green (&:where([data-theme=green], [data-theme=green] *));

:root {
  --default-primary-50: #d8ade5;
  --default-primary-900: #ae1fde;
  --blue-primary-50: #f1f9ff;
  --blue-primary-900: #3f88c4;
  --green-primary-50: #f6faf3;
  --green-primary-900: #1d843b;
}

@theme {
  --color-primary-50: var(--default-primary-50);
  --color-primary-900: var(--default-primary-900);
}

@layer theme {
  [data-theme="blue"] {
    --color-primary-50: var(--blue-primary-50);
    --color-primary-900: var(--blue-primary-900);
  }

  [data-theme="green"] {
    --color-primary-50: var(--green-primary-50);
    --color-primary-900: var(--green-primary-900);
  }
}

button {
  @apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>

<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>

<div class="
  mt-4 p-4 font-bold
  bg-primary-50 text-primary-900
  green:bg-primary-900 green:text-primary-50
">
  Hello World
</div>

And just as a fun fact, you can also use @custom-variants together with @variant.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";

@custom-variant blue (&:where([data-theme=blue], [data-theme=blue] *));
@custom-variant green (&:where([data-theme=green], [data-theme=green] *));

@theme {
  --color-primary-50: #d8ade5;
  --color-primary-900: #ae1fde;
}

@layer theme { 
  :root, :host {
    @variant blue {
      --color-primary-50: #f1f9ff;
      --color-primary-900: #3f88c4;
    }

    @variant green {
      --color-primary-50: #f6faf3;
      --color-primary-900: #1d843b;
    }
  }
}

button {
  @apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>

<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>

<div class="
  mt-4 p-4 font-bold
  bg-primary-50 text-primary-900
  blue:uppercase
  green:lowercase
  green:bg-primary-900 green:text-primary-50
">
  Hello World
</div>

And if you need access to the blue, green, and default colors without having a theme selected:

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@import "tailwindcss";

@custom-variant blue (&:where([data-theme=blue], [data-theme=blue] *));
@custom-variant green (&:where([data-theme=green], [data-theme=green] *));

@theme {
  --color-default-primary-50: #d8ade5;
  --color-default-primary-900: #ae1fde;
  --color-blue-primary-50: #f1f9ff;
  --color-blue-primary-900: #3f88c4;
  --color-green-primary-50: #f6faf3;
  --color-green-primary-900: #1d843b;
  
  --color-primary-50: var(--color-default-primary-50);
  --color-primary-900: var(--color-default-primary-900);
}

@layer theme {
  [data-theme="blue"] {
    --color-primary-50: var(--color-blue-primary-50);
    --color-primary-900: var(--color-blue-primary-900);
  }

  [data-theme="green"] {
    --color-primary-50: var(--color-green-primary-50);
    --color-primary-900: var(--color-green-primary-900);
  }
}

button {
  @apply px-2 py-1 mx-2 bg-blue-300 text-blue-900 rounded-md;
}
</style>

<button onclick="document.body.removeAttribute('data-theme')">Reset to Default</button>
<button onclick="document.body.setAttribute('data-theme', 'blue')">Blue Theme</button>
<button onclick="document.body.setAttribute('data-theme', 'green')">Green Theme</button>

<div class="
  mt-4 p-4 font-bold
  bg-primary-50 text-primary-900
  green:bg-primary-900 green:text-primary-50
">
  Hello World by current theme
</div>

<div class="
  mt-4 p-4 font-bold
  bg-default-primary-50 text-default-primary-900
">
  Only Default
</div>

<div class="
  mt-4 p-4 font-bold
  bg-blue-primary-50 text-blue-primary-900
">
  Only Blue
</div>

<div class="
  mt-4 p-4 font-bold
  bg-green-primary-50 text-green-primary-900
">
  Only Green
</div>

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

11 Comments

Still working on this: yet to have a successful build. Now, the error is "≈tailwindcss v4.1.6 Error: Cannot apply unknown utility class: badge" The suspected code is as follows: .badge { @apply inline-block relative truncate rounded-md font-medium border; @apply px-4 py-1.5 pl-9 text-base; } .badge-success { @apply badge text-success-700 bg-success-100 border-success-300; } So, "badge" is not a built-in Tailwind utility class. Is that why it cannot be @applied?
Apologies but the truth is: v3 to v4 migration is poorly documented yet. I guess it's inevitable for a relatively new tool. Too bad, the npx automated upgrade tool is a disappointment. It ran on this codebase but the process was nearly opaque. Would have been nice if it took in input concerning the codebase, and produced more detail about what it 1) needs to migrate 2) found to migrate 3) successfully migrated and 4) failed to migrate!
I think this is already a new question that would require a more detailed answer. I'll try to briefly respond in a comment nonetheless. The issue isn't with @apply, but with the .badge. You need to declare it as a utility: @utility badge { ... }. Additionally, instead of using @apply, it might be more appropriate to use native CSS with TailwindCSS variables, as adamwathan explained in a few of his posts.
I think the problem in many migrations stems from the fact that even in v3, classes and directives weren't always used according to the documentation. In v3, it should have looked like this: @layer utilities { .badge { ... } }, which is something the upgrade guide covers - explaining how to manually convert it for v4. I don't recommend using the upgrade tool; you'll never really learn the breaking changes that way. I manually migrated first of my projects.
Eventually succeeded with this, thanks to your selfless assitance. And NO THANKS this chaotic upgrade implementation! God bless!
|

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.