Cannot Apply Unknown Utility Class in Tailwind v4: The @reference Fix
Tailwind CSS · Intermediate · 6 min read · published
This article was written by Claude (Anthropic) and published automatically.
What this solves: Your Vue/Svelte scoped styles or CSS modules stopped compiling after upgrading Tailwind to v4, with @apply failing on classes that clearly exist. Here's why and the fix.
What Changed
If you upgraded to Tailwind CSS v4 (released January 2025) and your build now dies with Cannot apply unknown utility class: text-sm, nothing is wrong with your class names. What changed is compilation scope. In v3, PostCSS handed Tailwind a project-wide config object, so @apply worked in any file the pipeline touched. In v4 configuration lives in CSS itself — @import "tailwindcss", @theme, @utility — and each stylesheet is compiled in isolation. A Vue/Svelte/Astro <style> block, a .module.css file, or any CSS the bundler processes as its own chunk has no idea your theme exists.
The fix is the new @reference directive: it loads another stylesheet for reference only, making its utilities and theme variables resolvable without emitting a single byte of CSS.
The Old Way vs The New Way
Worked in v3, breaks in v4:
<!-- Card.vue -->
<style scoped>
.card {
@apply rounded-lg p-4 bg-surface text-sm;
}
</style>
<!-- v4: Cannot apply unknown utility class: rounded-lg -->
Works in v4:
<style scoped>
@reference "../assets/app.css"; /* the file with @import "tailwindcss" + @theme */
.card {
@apply rounded-lg p-4 bg-surface text-sm;
}
</style>
Or skip @apply entirely — v4 exposes every theme token as a real CSS variable, which needs no reference at all:
<style scoped>
.card {
border-radius: var(--radius-lg);
padding: --spacing(4);
background-color: var(--color-surface);
font-size: var(--text-sm);
}
</style>
Why It Was Added
In v3, the only way to give a component-scoped stylesheet access to your design tokens was to re-run the entire Tailwind config for that file. That's exactly what v3 did, invisibly, for every CSS module and every SFC style block — a large part of why v3 builds got slow in component-heavy apps. It also meant a stray @tailwind base in a component could silently duplicate the whole preflight into your bundle.
v4's Oxide engine treats each stylesheet as an independent compilation unit so it can cache and parallelise them. @reference makes the dependency explicit and side-effect-free: you state which stylesheet defines your vocabulary, and you get zero output from it. The error you hit is the engine refusing to guess.
How It Works Underneath
@reference behaves like Sass's @use in load-only mode. Tailwind parses the referenced file, collects @theme variables, @utility definitions, @custom-variants and plugins into an in-memory design system, then discards all generated rules. Only the utilities your @apply actually names get materialised into the current file.
flowchart TD
A["app.css<br/>@import tailwindcss<br/>@theme --color-surface"] -->|emits CSS| B["main bundle:<br/>preflight + utilities"]
C["Card.vue <style scoped>"] --> D{"@reference present?"}
D -->|no| E["empty design system<br/>@apply rounded-lg → ERROR<br/>Cannot apply unknown utility class"]
D -->|yes| F["parse app.css<br/>for definitions only"]
F --> G["theme vars + @utility<br/>registry in memory"]
G --> H["expand @apply<br/>inline"]
F -.->|CSS output discarded| X["(nothing emitted)"]
H --> I["component chunk:<br/>.card { border-radius:.5rem; ... }"]
Two consequences fall straight out of that mechanism. First, @reference "tailwindcss" resolves built-ins but not your custom @theme tokens — you must reference your own entry file. Second, referencing a big stylesheet from 200 components means parsing it 200 times, so watch-mode rebuilds get noticeably slower. That parse cost is the price @apply now charges.
Should You Adopt It Yet
v4 is production-ready and the ecosystem has moved; @reference is stable and documented, not experimental. But treat the error as a design signal rather than a chore. Adam Wathan has publicly discouraged @apply for years, and v4's isolation model makes the alternative genuinely better: reading var(--color-surface) in a scoped style is faster to compile, obvious to debug in devtools, and works in plain CSS with no Tailwind-aware tooling.
Wait on the upgrade if you depend on a JS-config-heavy plugin that hasn't shipped a v4 build, or if you must support browsers without @property and color-mix() support — v4 targets Safari 16.4+, Chrome 111+, Firefox 128+.
Migration Notes
rg '@apply' --glob '!**/node_modules' -lto list every file. Anything not your main entry stylesheet needs attention.- In each of those, add
@reference "<relative path to entry css>"as the first line. Paths are resolved relative to the file, and getting this wrong is the usual reason the error persists after "fixing" it. rg '@tailwind (base|components|utilities)'— replace with a single@import "tailwindcss"in one entry file only.- Convert
@layer components { .foo { @apply ... } }blocks to@utility foo { ... }, otherwise@apply fooelsewhere fails with the same message even with a reference. - Fix your editor separately: VS Code will flag
Unknown at rule @apply/@referenceuntil you associate*.csswith thetailwindcsslanguage mode. Don't disable CSS linting to hide it. - Where the rule is only two or three declarations, delete the
@applyand use theme variables. Fewer references means faster builds and one less v4-specific footgun.
Key takeaway: In Tailwind v4 every CSS file is compiled in isolation, so any file using @apply must declare its own `@reference` to your theme — or better, drop @apply and use the generated CSS variables directly.
Real-world challenge
After a Tailwind v3 → v4 upgrade, CI fails on one file: `Cannot apply unknown utility class: btn-primary`. The class is defined in your global `app.css` inside `@layer components { .btn-primary { @apply px-4 py-2 bg-blue-600; } }`, and a Vue component's scoped style does `@reference "../app.css"; .cta { @apply btn-primary; }`. Adding the @reference did not help. What's wrong and how do you fix it?
Diagnose. Two separate v4 rules are in play. @reference fixed the theme visibility problem, but the failing class isn't a utility — it's a plain CSS rule you wrote inside @layer components. In v4, @apply can only apply real utilities, and classes declared with bare CSS in a layer are not registered as utilities. That's why the error names your own class.
Confirm. Swap @apply btn-primary for a built-in like @apply px-4. If that compiles, the reference path is correct and the problem is purely that btn-primary isn't a utility.
Fix. Register it with @utility in your entry stylesheet so the engine knows about it:
/* app.css */
@import "tailwindcss";
@utility btn-primary {
padding: --spacing(2) --spacing(4);
background-color: var(--color-blue-600);
}
Now @reference "../app.css"; .cta { @apply btn-primary; } resolves. Better still, delete the indirection: .cta { background-color: var(--color-blue-600); } needs no reference at all and keeps the component chunk cheap to compile.
Grep for the rest. rg '@layer components' -A5 and rg '@apply' src will surface every other file that will hit the same error before CI does.