combining the CSS-first workflow
Tailwind v4 drops the central tailwind.config.js in favor of the @theme directive right inside CSS. For plain utility classes little changes in an Alpine template, but wherever an Alpine component used to read configuration values from JavaScript, breakpoints or color values for dynamic calculations, the source of truth shifts to CSS custom properties. Understanding that shift saves a lot of debugging when moving a v3 setup to v4.
Table of Contents
- 1. Leaving tailwind.config.js behind: what @theme actually changes
- 2. CSS variables instead of theme(): the new access path for Alpine
- 3. Practical example: a dynamic accent color via CSS variables and x-bind:style
- 4. Automatic content detection and dynamically assembled class names
- 5. Migration effort: what to watch for when moving v3 Alpine setups
- 6. @apply and component classes in the CSS-first workflow
- 7. Specifics in the Hyvä/Magento context
- 8. Build performance: the Oxide engine and its effect on the dev workflow
- 9. A checklist for a clean migration
- 10. Summary
- 11. FAQ
1. Leaving tailwind.config.js behind: what @theme actually changes
Through Tailwind v3, the entire design configuration, colors, spacing, breakpoints, font sizes, lived in a JavaScript file evaluated at build time. Anyone who needed those values programmatically, say to know the current breakpoint threshold inside an Alpine component for a calculation, either duplicated the values by hand or ran resolveConfig() in a separate build step to expose them as JSON.
Tailwind v4 flips that principle: configuration now lives as an @theme block right inside the entry CSS file and compiles down to real CSS custom properties. What used to be theme.colors.teal.500 in the old config becomes --color-teal-500 in the compiled CSS. That means those values are readable at runtime in the browser via getComputedStyle(), with no separate JSON export or build step, a far more direct access path for Alpine components than before.
/* app/design/frontend/Mironsoft/default/web/tailwind/tailwind-source.css */
@import "tailwindcss";
@theme {
--color-brand-accent: oklch(0.72 0.19 42);
--breakpoint-shop-nav: 64rem;
--spacing-card-gap: 1.25rem;
}
2. CSS variables instead of theme(): the new access path for Alpine
In Tailwind v3, custom CSS occasionally used the theme('colors.teal.500') function to reach configuration values outside utility classes. That function still exists in v4 for backward compatibility, but it is no longer the recommended path. Instead you reach directly into the generated CSS variable, for example var(--color-brand-accent), both in your own CSS and, this is the key difference, directly from Alpine.js through the DOM API.
For an Alpine component this means: instead of duplicating a color or breakpoint value in a separate JS constant, you read it at runtime straight from the computed style of the root element. That removes an entire class of drift bugs where the JS copy of a design token stops matching the actual CSS value after a redesign, simply because there is no second source left that could go stale.
// Alpine component reads a design token straight from CSS
Alpine.data('accentAwareBanner', () => ({
accentColor: '',
init() {
const styles = getComputedStyle(document.documentElement);
this.accentColor = styles.getPropertyValue('--color-brand-accent').trim();
},
}));
3. Practical example: a dynamic accent color via CSS variables and x-bind:style
A concrete use case: a campaign banner should switch its accent color depending on the active product category, where the available colors are maintained as Tailwind v4 theme tokens. Instead of managing a list of fixed Tailwind classes such as bg-teal-500, bg-orange-500 in JavaScript and assembling them dynamically, which collides with v4's content detection as described in the next section, you bind the color directly as a CSS variable through x-bind:style.
The advantage of this approach is that the utility class in the template stays static and therefore visible to Tailwind's scanner, while only the value of the CSS variable changes at runtime. That combines the robustness of Tailwind's static analysis with the flexibility of real runtime state, without giving up either benefit.
<div
x-data="{ accent: '--color-teal-500' }"
x-bind:style="{ '--banner-accent': `var(${accent})` }"
class="rounded-lg p-6 border-l-4"
style="border-color: var(--banner-accent, var(--color-brand-accent))"
>
<button x-on:click="accent = '--color-orange-500'" class="text-sm underline">
Switch category
</button>
</div>
4. Automatic content detection and dynamically assembled class names
Tailwind v4 automatically scans source files for class names and, unlike v3, no longer needs an explicit content list in the config. That works reliably as long as class names appear as complete, unchanged strings in the source. For Alpine components that build class names dynamically from substrings, for example `bg-${color}-500`, that is a problem, because the scanner only sees the template with the template literal and cannot resolve the runtime combination.
The reliable fix is to keep every actually needed class combination as a complete, static string somewhere in the source, for example in a comment line or a lookup table in the same template, so the scanner picks it up. An even more robust approach, and the more natural one in v4's CSS-first world, is to skip fixed class names altogether as shown in the previous section and work with CSS variables instead, which can change at runtime independent of the scanner.
5. Migration effort: what to watch for when moving v3 Alpine setups
When migrating an existing Hyvä or Alpine project from Tailwind v3 to v4, it pays to first inventory every place where JavaScript, and therefore Alpine components, read values from tailwind.config.js. That most often affects breakpoint values used in window.matchMedia() calls inside Alpine components, or color values needed dynamically for canvas or SVG drawing.
Every one of those spots needs to switch to reading via getComputedStyle() and CSS custom properties, which in practice is usually worth a small, reusable helper function in the Alpine codebase. Alongside that, it is worth reviewing every dynamically assembled class string in the project, since those are the ones most likely to silently break under v4's automatic content detection as described above, and only show up as missing styling in production.
6. @apply and component classes in the CSS-first workflow
For recurring combinations of utility classes that show up across several Alpine components, @apply remains usable in Tailwind v4, though in the spirit of CSS-first it is best kept inside an @layer components block right next to the @theme block, rather than scattered as utility chains in HTML. That keeps Alpine templates readable, especially when a component like an accordion or a dropdown has several states with their own class combinations.
Important: @apply resolves at build time and therefore knows nothing about Alpine's runtime state. For classes that need to change depending on x-data state, x-bind:class is still required, @apply component classes only make sense for the static part of the markup that always looks the same regardless of Alpine state.
@layer components {
.accordion-panel {
@apply rounded-lg border border-gray-200 px-4 py-3 transition-colors;
}
}
7. Specifics in the Hyvä/Magento context
In a Hyvä theme, the @theme configuration is typically maintained in a project-specific CSS source file under web/tailwind/, which imports the values inherited from the parent theme and selectively overrides them. Since Hyvä enforces a strict Content Security Policy, v4 changes nothing about how inline scripts are handled, every Alpine component that reads CSS variables remains a perfectly normal script snippet, registered the usual way through the Hyvä CSP component's registerInlineScript().
A practical benefit in the Magento context: since shop operators often need different accent colors per store view or website, those can now be overridden cleanly as CSS custom properties per theme variant, without touching a single line of Alpine JavaScript, as long as components consistently work through var() instead of fixed class names.
8. Build performance: the Oxide engine and its effect on the dev workflow
Tailwind v4 uses a new build engine written in Rust, internally known as Oxide, which delivers noticeably shorter build times than the v3 engine, especially for incremental rebuilds during development. For a Hyvä project with many Alpine components and frequent style tweaks, that means noticeably shorter wait times in watch mode, which speeds up the feedback loop when fine-tuning dynamic classes and CSS variables.
A side effect of the faster, more direct content detection is that errors from dynamically assembled class names surface faster, since the build no longer masks them behind long wait times. Anyone consistently applying the switch to CSS variables described in the fourth section benefits twice: faster builds and fewer cases where the scanner could miss anything in the first place.
9. A checklist for a clean migration
Before migrating, a short, ordered approach pays off: first list every JS access to the old tailwind.config.js, then convert each spot individually to CSS custom properties, next search for every dynamically assembled class string in Alpine templates and replace it with a CSS variable binding, and finally run the build in watch mode and visually re-check every affected component.
Following that order avoids the most common migration problem: part of the classes suddenly missing in the production build because the scanner no longer recognized a dynamically built combination, an error that often stays invisible in local development with unpurged CSS and only shows up after deployment.
| Aspect | Tailwind v3 | Tailwind v4 | Effect on Alpine |
|---|---|---|---|
| Config location | tailwind.config.js |
@theme block in CSS |
No JS import needed, values readable via CSS |
| Access from JS | resolveConfig() / manual copy |
getComputedStyle() on CSS variable |
One single source of truth, no duplicates |
| Dynamic classes | content list maintained manually in config | Automatic scan, no content list | Assembled strings need special handling |
| Build engine | JS-based (PostCSS plugin) | Rust-based (Oxide) | Faster watch rebuilds while tuning Alpine |
| @apply usage | Free anywhere in CSS | Recommended inside @layer components |
Clearer split between static markup and Alpine state |
Mironsoft
Alpine.js interactivity for Hyvä frontends
A Hyvä frontend that needs more interactivity, but without React overhead?
We build interactive frontend components for Hyvä themes with Alpine.js, lightweight and without build-step complexity, from simple toggles to complex form flows.
Custom Components
Develop interactive Alpine.js components for specific shop requirements.
Performance Review
Review existing Alpine.js implementations for reactivity pitfalls and performance.
Team Training
Bring developers up to speed on Alpine.js patterns for Hyvä themes hands-on.
10. Summary
Tailwind v4 with Alpine.js
Core idea
Tailwind v4 replaces tailwind.config.js with @theme in CSS, so Alpine now reads design tokens via CSS custom properties instead of JS config.
Practical benefit
Dynamic accent colors and breakpoints can be read straight from the root element's computed style without duplicating data.
Biggest pitfall
Tailwind classes assembled dynamically from substrings are easily missed by v4's automatic content detection.
Recommendation
Consistently bind Alpine to design tokens through CSS variables rather than through dynamically built class names.