Tailwind v4 @reference: Fix @apply in Blade & Scoped CSS

Tailwind v4's @reference directive fixes 'unknown utility class' when @apply runs in scoped or separately-bundled CSS. Point it at your theme the right way.

Steven Richardson
Steven Richardson
· 7 min read

You upgrade a Laravel app to Tailwind v4, split your CSS into a second Vite entry for an admin bundle, and the build dies on @apply bg-brand: Cannot apply unknown utility class: bg-brand. The exact line worked in v3. Nothing about your theme changed — but the file it lives in did, and in v4 that's the thing that matters.

The fix is the tailwind @reference directive. It tells an isolated stylesheet where your theme lives so @apply can resolve again, without duplicating a single byte of CSS into that bundle. Here's why v4 breaks the old behaviour, how to wire @reference correctly, and when to skip it entirely.

Why @apply breaks in scoped CSS#

In v3, a global tailwind.config.js and a single PostCSS pass meant every file was processed with the same context. Your tokens were available everywhere by definition. v4 is CSS-first: @apply and @variant resolve against the theme that's defined in — or imported into — the same file. Your @import "tailwindcss" and @theme block live in resources/css/app.css. Any CSS that the bundler compiles on its own — a second Vite entry, a CSS module, a Vue or Svelte <style> block — is processed in isolation and has no idea your tokens exist.

So the moment your CSS stops being one file, @apply loses the theme. Here's the classic Laravel trigger — a multi-entry Vite setup:

// vite.config.js
export default defineConfig({
  plugins: [
    laravel({
      input: ['resources/css/app.css', 'resources/css/admin.css'],
      refresh: true,
    }),
    tailwindcss(),
  ],
});

app.css holds the theme. admin.css is bundled separately and tries to use a token from it:

/* resources/css/admin.css — compiled on its own, theme not in scope */
.admin-badge {
  @apply bg-brand text-white rounded-full px-3 py-1;
}
Cannot apply unknown utility class: bg-brand

This is also why tailwind v4 @apply not working shows up the day after an upgrade — the v3-to-v4 move itself splits config out of JavaScript and into CSS. If you haven't run that migration yet, the Laravel Tailwind v3 to v4 upgrade tool walkthrough covers the framework swap; this article picks up at the scoped-CSS fallout it leaves behind.

Point the @reference directive at your theme#

The fix is one line at the top of the isolated file. @reference imports your real stylesheet for reference only — Tailwind reads the theme, custom utilities, and custom variants to resolve @apply, but emits no CSS from that import:

/* resources/css/admin.css */
@reference "./app.css";

.admin-badge {
  @apply bg-brand text-white rounded-full px-3 py-1;
}

Where app.css defines the token the way it always has:

/* resources/css/app.css */
@import "tailwindcss";

@theme {
  --color-brand: oklch(62% 0.19 256);
}

The path is relative to the file containing @reference, not to your project root — get the ../ wrong and you'll see a module-not-found error rather than an unknown-utility one. If you'd rather not count directory hops, v4 supports Node subpath imports: map "#app.css" in package.json and write @reference "#app.css"; from anywhere.

The same rule covers any tailwind scoped css context. In an Inertia + Vue app, the <style> block is its own bundle:

<style>
  @reference "../../css/app.css";

  h1 {
    @apply text-2xl font-bold text-brand;
  }
</style>

And if you ship a Blade component with a paired stylesheet that Vite treats as a separate input — the tailwind @apply blade component case — @reference your app.css at the top of that file and @apply resolves exactly as it does in your main bundle.

@reference "tailwindcss" vs your own stylesheet#

There are two forms, and picking the wrong one is the second-most-common way to stay stuck on "unknown utility class". @reference "tailwindcss" pulls in only Tailwind's default theme. That's fine when the isolated file touches nothing but stock utilities and you have made zero @theme customisations:

/* No custom tokens anywhere? Reference the framework directly. */
@reference "tailwindcss";

.prose-callout {
  @apply rounded-lg border px-4 py-3 text-sm;
}

The trap: the instant you @apply a custom token, referencing tailwindcss still throws, because the default theme has never heard of brand. To reference your stylesheet — with its @theme, @utility, and @custom-variant definitions — point @reference at app.css instead. The rule of thumb is short: a customised theme means reference your own stylesheet; a vanilla theme means reference tailwindcss. When in doubt, reference app.css — it's a strict superset.

Reach for CSS variables before @apply#

@reference works, but it isn't free. Tailwind re-parses the referenced stylesheet once for every file that references it. One admin bundle is nothing. Hundreds of Vue or Svelte <style> blocks each referencing app.css is a different story — build times balloon, and Tailwind's own guidance warns it can run the build out of memory.

You usually don't need it. Every --color-* you declare in @theme is also a real CSS variable at runtime, so a component can read it directly and skip both @apply and the reference:

/* No @reference, no reparse, no build penalty. */
.admin-badge {
  background-color: var(--color-brand);
  border-radius: var(--radius-full);
  padding: --spacing(1) --spacing(3);
}

Cleaner still is to not write component CSS at all. Put the utilities in the markup — class="bg-brand rounded-full px-3 py-1" — and @apply never enters the picture. That's the right default for Blade; the source-inline safelisting for dynamic Blade classes post covers the one case where utilities-in-markup needs a nudge to be detected. When you genuinely do want a reusable named class, register it with the Tailwind v4 @utility directive so it lands in the utilities layer and earns variants for free, rather than hand-writing a rule that @apply then can't even see.

Gotchas and Edge Cases#

@reference "tailwindcss" does not carry your tokens. If your error is specifically about a custom utility like bg-brand, swapping tailwindcss for ./app.css is the fix — the framework default simply doesn't contain your @theme colours.

@import and @reference are not interchangeable. @import "tailwindcss" inside an isolated file pulls the entire framework into that bundle, duplicating output; @reference pulls it in for resolution only and emits nothing. If a separate bundle suddenly ballooned in size, you reached for @import where you wanted @reference.

@reference only exposes theme tokens, custom utilities, and custom variants — not arbitrary hand-written classes. @apply my-card where .my-card is a plain rule defined elsewhere won't resolve, because it was never registered as a utility. Promote it with @utility if you need it to be apply-able.

Don't @reference a file from itself. Referencing app.css at the top of app.css creates a circular import; the theme is already in scope there, so @apply works without any reference at all.

The build only re-resolves when it recompiles. If a freshly added @reference still throws, the dev server hasn't picked up the change — restart npm run dev or run a fresh npm run build.

Wrapping Up#

The mental model is one sentence: @apply resolves against the current file's theme, so any separately-bundled CSS needs @reference "./app.css" to see your tokens — use tailwindcss only for the stock theme, and prefer plain CSS variables in components to keep builds fast.

From here, the custom variants that @reference also unlocks in scoped CSS are worth a look — the Tailwind v4 CSS-first dark mode with @custom-variant walkthrough shows the @custom-variant side of the same engine. And since @apply is only ever as good as the tokens behind it, the Tailwind v4 OKLCH and color-mix design tokens post covers defining those tokens once so every bundle resolves the same values.

FAQ#

Why is @apply not working in Tailwind v4?

Because v4 resolves @apply against the theme defined in or imported into the same file. Your @import "tailwindcss" and @theme tokens live in app.css, so any stylesheet the bundler compiles separately — a second Vite entry, a CSS module, a framework <style> block — has no access to them and throws "Cannot apply unknown utility class". Add @reference "./app.css"; to the top of that file to bring the theme back into scope.

What does the @reference directive do in Tailwind CSS?

@reference imports a stylesheet for reference only. Tailwind reads its theme variables, custom utilities, and custom variants so @apply and @variant can resolve, but it emits none of that stylesheet's CSS into the current bundle. It exists specifically for files compiled in isolation, where you need the theme available for resolution without duplicating the whole framework into the output.

How do I use @apply in a Vue or Blade scoped stylesheet?

Add @reference pointing at your main stylesheet as the first line of the <style> block or the component's CSS file, for example @reference "../../css/app.css";, then use @apply as normal underneath it. The relative path is resolved from the file you're writing in, not the project root. For a Blade component with a separate Vite-bundled stylesheet, the same single line at the top makes your tokens resolvable inside that file.

Does @reference duplicate my CSS output?

No — that's the entire point of it. @reference imports a stylesheet for reference without including its styles, so utilities resolve while nothing extra is written to the bundle. The directive you want to avoid in an isolated file is @import "tailwindcss", which does pull the full framework in and duplicate output. If a separate bundle grew unexpectedly, check that you used @reference rather than @import.

When should I use @reference "tailwindcss" vs my own stylesheet?

Use @reference "tailwindcss" only when the isolated file uses nothing but Tailwind's default utilities and you've made no @theme customisations. The moment you @apply a custom token like bg-brand, the default theme won't contain it and you'll still get "unknown utility class" — reference your own app.css instead, since it carries your @theme, custom utilities, and custom variants. When unsure, reference app.css; it's a strict superset of the default theme.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.