Fix "Cannot apply unknown utility class border-border"
Reproduced on Tailwind 4.1.4 to 4.3.3 and 3.4.19: the four wordings of this error, the @config path that resolves, the hsl() trap in @theme, and the two v3 causes.
TL;DR#
border-border is not a utility Tailwind ships. It is the border-* colour utility for a theme colour named border, which shadcn/ui's tailwind.config.js defines as hsl(var(--border)). Tailwind CSS v4 stops reading that file unless your CSS points at it, so the colour is missing and @apply border-border throws. Replace the three @tailwind lines with @import "tailwindcss"; and add @config "../tailwind.config.js"; (the path is relative to the CSS file), or move the colours into @theme inline. On Tailwind v3, the same class "does not exist" when a leftover tailwind.config.js shadows tailwind.config.ts, or when the config sets a prefix.
The error, in its four wordings#
With the shadcn/ui v3 stylesheet left unchanged under Tailwind v4, @tailwindcss/postcss 4.3.3 stops with:
Error: Cannot apply unknown utility class `border-border`. Are you using CSS modules or similar and missing `@reference`? https://tailwindcss.com/docs/functions-and-directives#reference-directiveAfter a stack trace into tailwindcss/dist/lib.js, PostCSS repeats the message with a location:
CssSyntaxError: tailwindcss: /w/v4/app/globals.css:1:1: Cannot apply unknown utility class `border-border`. Are you using CSS modules or similar and missing `@reference`? https://tailwindcss.com/docs/functions-and-directives#reference-directive
> 1 | @tailwind base;
| ^
2 | @tailwind components;
3 | @tailwind utilities;The Stack Overflow question this page answers, about a Next.js app, quotes two families of wording: its body opens with the Tailwind v3 sentence, while a code comment in the body and the start of the title carry the v4 one. Which one you get depends on the version and on what the stylesheet loaded, and the reproduction produced all four. Besides the hinted form above, these three lines come from three separate runs (4.3.3 with a theme loaded, 4.1.7, and 3.4.19), one line each:
Error: Cannot apply unknown utility class `border-border`
Error: Cannot apply unknown utility class: border-border
CssSyntaxError: /w/v3/app/globals.css:1:1: The `border-border` class does not exist. If `border-border` is a custom class, make sure it is defined within a `@layer` directive.- The sentence with the
@referencehint (the blocks above): tailwindcss 4.1.8 or later, and a stylesheet compiled without any theme. - The same sentence without the hint: 4.1.8 or later, with a theme loaded that has no
bordercolour. - The colon form,
Cannot apply unknown utility class: border-border: versions before 4.1.8. The 4.1.8 changelog entry changed it under "Improve error messages when@applyfails" (PR #18059). - "The
border-borderclass does not exist": Tailwind v3.
Whenever the reproduction printed a code frame, it pointed at line 1 of the stylesheet rather than at the @apply line: the caret does not locate the cause.
Why Tailwind v4 cannot find border-border#
Three facts combine.
The class comes from your config, not from Tailwind#
Border colour utilities are generated from the theme's colours, and border-border only exists because shadcn/ui's v3-era CLI wrote a colour named border into tailwind.config.js (its template at shadcn-ui 0.4.1):
// tailwind.config.js written by the shadcn/ui v3 CLI (excerpt)
module.exports = {
darkMode: ["class"],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
},
},
},
plugins: [require("tailwindcss-animate")],
};Its globals.css then sets the variables (--border: 214.3 31.8% 91.4%; in the slate theme) and applies the colours in a base layer, which is the file in the question. The bg-background and text-foreground on the next line come from the same config.
Tailwind v4 does not read that file on its own#
The upgrade guide says so in one line: "JavaScript config files are still supported for backward compatibility, but they are no longer detected automatically in v4." The Next.js setup for Tailwind v4 is @tailwindcss/postcss in postcss.config.mjs plus @import 'tailwindcss'; in the global CSS, and nothing in it names a config file. A project created or upgraded on v4 with a v3-style shadcn/ui config therefore compiles without the border colour. If you run npx @tailwindcss/upgrade to do this migration for you and the official upgrade tool fails on Windows with Could not load the configuration file, run it without the @next tag.
The old directives load no theme at all#
The guide's Removed @tailwind directives section replaces the three @tailwind lines with @import "tailwindcss";. Left in place under v4, they compile with an empty theme, and the @apply code in apply.ts checks for exactly that before it throws. Its comment reads: "When the theme is empty, it means that no theme was loaded and @import "tailwindcss", @reference "app.css" or similar is very likely missing." That is why a global stylesheet gets a hint about CSS modules.
The reproduction separates the two layers. Swapping the three directives for @import "tailwindcss", and changing nothing else, turned the message into the plain one, and the build still failed. Adding the @config line on top of it made the build pass. The Stack Overflow answer makes both changes at once: the import is how v4 loads Tailwind itself, and the @config line is the one that brings border-border back.
Fix: load the config, or move the colours into CSS#
1. Check the PostCSS plugin first#
Tailwind v4 runs through its own PostCSS package. In a Next.js project, postcss.config.mjs needs only this, as in the Next.js docs:
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};If the build stops earlier, with "It looks like you're trying to use tailwindcss directly as a PostCSS plugin", the file still lists the v3 plugin: that error has its own fix, on the same v3-to-v4 upgrade path. Vite projects may use @tailwindcss/vite instead, as the upgrade guide recommends; the reproduction on this page used the PostCSS plugin.
2. Quickest: point Tailwind at your existing config with @config#
@import "tailwindcss";
@config "../tailwind.config.js";
/* your :root and .dark variables, unchanged */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}The path is resolved from the folder that contains the CSS file, not from the project root:
app/globals.csswith the config at the project root:@config "../tailwind.config.js";src/app/globals.css:@config "../../tailwind.config.js";, the path in the Stack Overflow answer, which only fits that layout. Copied intoapp/globals.css, it fails with:
Error: Can't resolve '../../tailwind.config.js' in '/w/v4/app'A tailwind.config.ts loads the same way (@config "../tailwind.config.ts";). In the reproduction, Node 22 printed a MODULE_TYPELESS_PACKAGE_JSON warning while loading the TypeScript file, and the build still passed. The config's plugins come along too: shadcn/ui's tailwindcss-animate still generated .animate-in through @config. What does not come along, per the directives reference, are the corePlugins, safelist and separator options. The same page files @config under directives that "exist solely for compatibility with Tailwind CSS v3.x", so treat this step as the unblock and step 3 as the v4-native layout.
3. The v4 layout: colours in @theme inline, no JavaScript config#
shadcn/ui's Tailwind v4 guide maps each colour variable to a --color-* theme variable inside @theme inline, which makes border-border exist without any config file. Which line you write depends on the format of your variables.
Variables from a v3 project hold bare HSL channels (--border: 214.3 31.8% 91.4%;). Keep hsl() in the theme:
@import "tailwindcss";
@theme inline {
--color-border: hsl(var(--border));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
}shadcn/ui's v4 layout wraps the values themselves (--border: hsl(214.3 31.8% 91.4%);, or oklch() in its newer themes) and maps them without a wrapper: --color-border: var(--border);. Its guide lists the steps: move :root and .dark out of @layer base, wrap the colour values in hsl(), add the inline option, and remove the hsl() wrappers from the theme.
Mixing the two is the trap. Bare channels with --color-border: var(--border); compile with no error, and grep -n on the output shows the rule pointing at a value that is not a colour:
193: border-color: var(--border);
231: --border: 214.3 31.8% 91.4%;
258: border-color: var(--border);214.3 31.8% 91.4% is three numbers, not a colour, so that border-color declaration cannot give the border the shadcn/ui colour. The inline keyword itself matters because these theme variables point at other variables: per Tailwind's theme docs, the utility then uses the variable's value directly, and "Without using inline, your utility classes might resolve to unexpected values because of how variables are resolved in CSS." Add one --color-* line for every colour your @apply rules and markup use (shadcn/ui's list also covers input, ring, primary, muted and the rest), and keep tailwind.config.js for now.
Colours are only part of a shadcn/ui v3 config. It also sets darkMode: ["class"], the container, the borderRadius scale, the accordion keyframes and animations, and the tailwindcss-animate plugin. Deleting the file removes all of them and nothing reports it: in the reproduction, animate-in compiled through @config but was silently absent once the plugin was gone (step 4). The upgrade tool's output, excerpted in step 4, has a CSS form for everything but the plugin: @custom-variant dark (&:is(.dark *)); for class-based dark mode, an @utility container block, --radius-lg, --radius-md and --radius-sm in the theme, and --animate-accordion-down and --animate-accordion-up with their @keyframes. The plugin needs @plugin "tailwindcss-animate";, which the tool did not write; step 4 adds it by hand. Delete the JavaScript config once each of those has moved; until then, keep the @config line from step 2. The @custom-variant dark line only enables dark:-prefixed utilities and gives no theme colour a second value, so an unprefixed class follows .dark only through a variable override, as in the Tailwind v4 dark class fix.
4. Or let the upgrade tool write it, while the project is still on v3#
npx @tailwindcss/upgradeThe upgrade guide says the tool needs Node.js 20 or higher and suggests running it in a new branch, and its source stops with "Git directory is not clean" when there are uncommitted changes. On the shadcn/ui v3 TypeScript project of the reproduction, it printed Upgrading from Tailwind CSS v3.4.19, then:
- deleted
tailwind.config.tsand wrote its colours intoapp/globals.cssas@themevariables,--color-border: hsl(var(--border));included; - switched
postcss.config.jsto@tailwindcss/postcssand upgraded tailwindcss to 4.3.3; - left a stylesheet that compiled.
The top of the app/globals.css it wrote, with the colour lines after the first two cut:
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 1400px) {
max-width: 1400px;
}
}
@theme {
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
/* ...the other --color-* lines, cut here... */
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}Its output is a plain @theme block with hsl() references. shadcn/ui's Tailwind v4 guide describes that same codemod output, says "This works.", and then moves it to @theme inline with the steps listed in step 3.
It did not carry the plugins: [require("tailwindcss-animate")] entry across: after the upgrade, animate-in did not compile until this line went under the import.
@import 'tailwindcss';
@plugin "tailwindcss-animate";shadcn/ui deprecated tailwindcss-animate on 19 March 2025 in favour of tw-animate-css, which new projects get by default and load with @import "tw-animate-css";, as the changelog at the end of its Tailwind v4 guide records.
Verify the fix#
Compile the stylesheet on its own and look for the rule. With postcss-cli installed, the project's own postcss.config.mjs applies:
npm install -D postcss-cli
npx postcss app/globals.css -o tw-check.css
grep -n "border-color" tw-check.cssExpect border-color: hsl(var(--border)); after step 2 or after step 3 with v3 variables, border-color: var(--border); with shadcn/ui's v4 layout, or border-color: var(--color-border); after the upgrade tool. Then restart next dev and run the build.
Check the version of the PostCSS plugin too, with npm ls @tailwindcss/postcss. From 4.1.5 to 4.1.13, a failing compile did not stop the build: the plugin logged the error, emptied the stylesheet and carried on. In the reproduction, 4.1.4 exited 1 and wrote nothing; 4.1.5, 4.1.8 and 4.1.13 printed the error, exited 0 and wrote a one-byte stylesheet; 4.1.14 exited 1 again. On 4.1.13, three commands in a row printed the error, an exit code of 0 and the size of a one-byte file:
Error: Cannot apply unknown utility class `border-border`
postcss exit code: 0
1 out.cssThe behaviour arrived with "PostCSS: Ensure that errors in imported stylesheets are recoverable" in the 4.1.5 changelog (PR #17754) and left with "Re-throw errors from PostCSS nodes" in the 4.1.14 changelog (PR #18373). On those versions, a build that passes with an unstyled page can hide this same error, so read the terminal output rather than the exit status.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When the @reference hint is right: CSS modules, Vue and Svelte blocks#
A stylesheet compiled on its own, such as a CSS module or a <style> block in Vue, Svelte or Astro, sees no theme. @apply border-border fails there with the hint even after globals.css is fixed. The reproduction compiled an app/card.module.css alone and got the same hinted error; pointing @reference at the main stylesheet fixed it:
@reference "./globals.css";
.card {
@apply border-border;
}The output held only Tailwind's licence comment and the module's own rule:
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
.card {
border-color: hsl(var(--border));
}That matches the @reference docs: the directive imports the main stylesheet "for reference without actually including the styles". A project that only uses the default theme can write @reference "tailwindcss"; instead.
Still on Tailwind v3: a second config file, or a prefix#
The second and third answers both reproduce on tailwindcss 3.4.19 with its own PostCSS plugin.
A leftover tailwind.config.js next to tailwind.config.ts. Tailwind v3 looks for ./tailwind.config.js, then the .cjs, .mjs, .ts, .cts and .mts names, and loads the first one that exists (resolveConfigPath.js). A default file left by npx tailwindcss init therefore wins over the shadcn/ui TypeScript config. The tell is the warning printed just before the error, because the default file has content: []:
warn - The `content` option in your Tailwind CSS configuration is missing or empty.
warn - Configure your content sources or your generated CSS will be missing styles.
warn - https://tailwindcss.com/docs/content-configuration
CssSyntaxError: /w/v3/app/globals.css:1:1: The `border-border` class does not exist. If `border-border` is a custom class, make sure it is defined within a `@layer` directive.Deleting tailwind.config.js, the fix in the second answer, made the same build pass.
A prefix. With prefix: "tw-" in the config, @apply border-border fails and @apply tw-border-border; works, as the third answer says; the base layer's other rule needs @apply tw-bg-background tw-text-foreground;. shadcn/ui's components.json has a prefix setting under tailwind, and its v3 config templates carry a matching prefix field. In v4, prefixes look like variants instead (tw:flex), as the upgrade guide shows.
After the stylesheet compiles#
Every output block on this page comes from the reproduction, run on 6 October 2026 in a node:22-alpine container (Node 22.23.3, postcss-cli 12.0.0), with shadcn/ui's own v3 globals.css and config templates as input, on tailwindcss and @tailwindcss/postcss 4.3.3, 4.1.4, 4.1.5, 4.1.7, 4.1.8, 4.1.13 and 4.1.14, and on tailwindcss 3.4.19. Once the stylesheet compiles, the next thing a shadcn/ui setup depends on is not CSS at all: its components are imported through the alias that its installation guide asks you to configure, either @/* in the tsconfig.json paths or package.json imports. If an import such as @/components/ui/button then fails with Cannot find module, continue with tsconfig paths not working in Next.js; if it only fails in a Vercel build, Next.js Module not found: Can't resolve covers the alias and case-sensitivity causes there.
Further reading: Tailwindcss not working with Next.js: fix the config. Also see Fix: TypeError: Invalid PostCSS Plugin found at: plugins[0].
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Tailwindcss not working with Next.js: fix the config
A Next.js build that finishes clean but ships a page with zero Tailwind styling almost always has one wrong setting: the paths Tailwind scans for class names.
Fix: TypeError: Invalid PostCSS Plugin found at: plugins[0]
A postcss.config.js that lists plugin names as plain strings compiles nothing — the array syntax needs each entry already resolved, not a package name.
Fix Tailwind v4: dark class changes no background colour
Keep one bg-bgContrast class while changing its inherited colour, with real browser checks for the wrong theme syntax and the inline-token trap.


