Dynamic Theme Switching with CSS Variables and Tailwind

Building flexible, themeable interfaces doesn’t have to be complex.

By combining Tailwind’s data attribute selectors with environment variables, you can create a clean system that switches themes dynamically without JavaScript or complex state management. Shout out to Matt Evans for teaching me this trick in a recent collaboration.

I’m using Astro below, but you can adapt this approach to any framework, heck you don’t even need a framework at all.

The Setup

Start by exposing your theme as a client-side environment variable in your Astro config:

// astro.config.mjs
export default defineConfig({
  env: {
    schema: {
      THEME: envField.string({ context: "client", access: "public" }),
    },
  },
});

This allows you to control themes at the environment level—perfect for different deployments or runtime switching.

Layout Integration

In your layout, pass the theme to a data attribute on the body:

---
import { THEME } from "astro:env/client";
---
<body class="group/body" data-theme={THEME}>
  <slot />
</body>

The group/body class is key—it creates a Tailwind group that child components can reference.

Theme-Aware Components

Now components can style themselves based on the active theme using Tailwind’s group-data-[] modifier:

<div class={`
  border-gray-300
  group-data-[theme=brutalist]/body:border-primary
  group-data-[theme=round]/body:rounded-full
`}>
  <input class="group-data-[theme=round]/body:px-4" />
</div>

Why This Works

It’s pretty simple:

  • No JavaScript required: Themes switch purely through CSS selectors
  • Environment flexibility: Set THEME=brutalist or THEME=round at deploy time
  • Component isolation: Each component handles its own theme variations
  • Performance: No runtime theme switching overhead

While we have a fixed theme set via environment variables, you can easily switch themes dynamically using JavaScript that updates the data-theme attribute on the body element.

CSS Variables Integration

For more complex theming, combine this approach with CSS custom properties:

[data-theme="brutalist"] {
  --primary: #000;
  --border-radius: 0;
}

[data-theme="round"] {
  --primary: #3b82f6;
  --border-radius: 9999px;
}

Then reference these in your Tailwind config or components as needed.

This pattern gives you the flexibility of environment-controlled theming with the power of Tailwind’s utility classes.