Back to Blog 16 SEP 2025

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.