Skip to content
Development

A Tailwind v4 migration that also required patching a shared package

By Victor Da Luz
astrotailwinddev-logsite

This site had been pinned to Tailwind v3 since a dependabot grouped update tried to bump it to v4 and broke the build outright. The real migration sat in the backlog for a while. When I finally picked it up, the actual work wasn’t the config swap - it was finding a bug that a clean build would never have shown me.

The mechanical part of a v3-to-v4 Tailwind migration on Astro is well documented: swap the @astrojs/tailwind integration for the native @tailwindcss/vite plugin, and replace @tailwind base/components/utilities with @import "tailwindcss"; in your CSS. The part I had to actually verify was whether I could keep my existing tailwind.config.mjs instead of rewriting everything into v4’s CSS-first @theme format. Turns out yes, via a @config directive - but I didn’t want to just trust the docs on that. This config file does two load-bearing things: it points at content living inside node_modules/@vdaluz/* packages, and it defines colors using the rgb(var(--x) / <alpha-value>) placeholder syntax. Either one silently breaking would be invisible in a build log.

I’d been burned by exactly this kind of silent breakage before (a Tailwind content glob that missed a package and just never generated the classes it needed, no error anywhere). So instead of building and calling it done, I re-ran that same regression check: load the privacy page, grab the shared PrivacyExplainer links, and check their computed text-decoration-line. Still underline. The node_modules content glob survived the migration. Same story for the alpha-value colors - checked a real computed box-shadow/color value in the browser instead of assuming the placeholder syntax still worked in v4.

What I didn’t expect was finding a real bug in a completely different repo. Tailwind v4 renamed or removed a handful of “bare” utility names - plain shadow and flex-grow among them. I grepped my own markup for those names before starting (found one: a leftover bare rounded class on the 404 page, easy fix). But I also grepped the vendored @vdaluz/astro-blog package sitting in node_modules, since its components render on every blog post here. Its RelatedPosts.astro component used both shadow and flex-grow. Since that’s a pinned tarball dependency, not code I can edit inside this repo, that would have shipped with silently missing shadows and a broken flex layout on every related-posts card. No build error, no warning. Just a slightly worse-looking card until someone happened to notice.

Fixing it wasn’t as simple as a find-and-replace rename, though. That same package also serves vdaluz.com, which is still on Tailwind v3. And v4 didn’t just remove shadow - it renamed shadow-sm to mean what v3’s bare shadow used to mean. So swapping shadow for shadow-sm in the shared package would have changed vdaluz.com’s visual weight too, as a side effect of a migration that has nothing to do with vdaluz.com. I ended up writing the shadow as an explicit arbitrary value - the literal CSS Tailwind v3’s default shadow produces - so it renders byte-identical under both versions. flex-grow was simpler: grow has been a safe alias since Tailwind v3.3, no visual difference on either version. Released as astro-blog v0.6.2, tested via the package’s own release process (pack, install into a real consumer, check and build) before pointing this repo’s dependency at the new tag.

The lesson that’ll stick with me: a dependency bump’s blast radius isn’t bounded by your own src/ folder. If you’re consuming shared packages, the markup that matters is whatever’s actually installed in node_modules, and it’s worth grepping that too before you trust a clean build.

Related reading