tailwind-csstailwind-css-4

Dynamic custom color themes in Tailwindcss v4


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?


Solution

  • 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 { 
      * {
        @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>