diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc14695211ce..c1ba5950d0c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Improve error messages when `@apply` fails ([#18059](https://github.com/tailwindlabs/tailwindcss/pull/18059))
+
### Fixed
- Upgrade: Do not migrate declarations that look like candidates in `
+ `,
+ },
+ },
+ async ({ exec, expect }) => {
+ expect.assertions(1)
+
+ try {
+ await exec('pnpm vite build')
+ } catch (error) {
+ let [, message] =
+ /error during build:([\s\S]*?)file:/g.exec(
+ stripVTControlCharacters(error.message.replace(/\r?\n/g, '\n')),
+ ) ?? []
+ expect(message.trim()).toMatchInlineSnapshot(
+ `"[@tailwindcss/vite:generate:build] Cannot apply unknown utility class \`text-red-500\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive"`,
+ )
+ }
+ },
+)
diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts
index 87361804f7ae..691df2920a6e 100644
--- a/packages/tailwindcss/src/apply.ts
+++ b/packages/tailwindcss/src/apply.ts
@@ -4,6 +4,7 @@ import { compileCandidates } from './compile'
import type { DesignSystem } from './design-system'
import type { SourceLocation } from './source-maps/source'
import { DefaultMap } from './utils/default-map'
+import { segment } from './utils/segment'
export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let features = Features.None
@@ -176,7 +177,68 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
let candidates = Object.keys(candidateOffsets)
let compiled = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
- throw new Error(`Cannot apply unknown utility class: ${candidate}`)
+ // When using prefix, make sure prefix is used in candidate
+ if (designSystem.theme.prefix && !candidate.startsWith(designSystem.theme.prefix)) {
+ throw new Error(
+ `Cannot apply unprefixed utility class \`${candidate}\`. Did you mean \`${designSystem.theme.prefix}:${candidate}\`?`,
+ )
+ }
+
+ // When the utility is blocklisted, let the user know
+ //
+ // Note: `@apply` is processed before handling incoming classes from
+ // template files. This means that the `invalidCandidates` set will
+ // only contain explicit classes via:
+ //
+ // - `blocklist` from a JS config
+ // - `@source not inline(…)`
+ if (designSystem.invalidCandidates.has(candidate)) {
+ throw new Error(
+ `Cannot apply utility class \`${candidate}\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes`,
+ )
+ }
+
+ // Verify if variants exist
+ let parts = segment(candidate, ':')
+ if (parts.length > 1) {
+ let utility = parts.pop()!
+
+ // Ensure utility on its own compiles, if not, we will fallback to
+ // the next error
+ if (designSystem.candidatesToCss([utility])[0]) {
+ let compiledVariants = designSystem.candidatesToCss(
+ parts.map((variant) => `${variant}:[--tw-variant-check:1]`),
+ )
+ let unknownVariants = parts.filter((_, idx) => compiledVariants[idx] === null)
+ if (unknownVariants.length > 0) {
+ if (unknownVariants.length === 1) {
+ throw new Error(
+ `Cannot apply utility class \`${candidate}\` because the ${unknownVariants.map((variant) => `\`${variant}\``)} variant does not exist.`,
+ )
+ } else {
+ let formatter = new Intl.ListFormat('en', {
+ style: 'long',
+ type: 'conjunction',
+ })
+ throw new Error(
+ `Cannot apply utility class \`${candidate}\` because the ${formatter.format(unknownVariants.map((variant) => `\`${variant}\``))} variants do not exist.`,
+ )
+ }
+ }
+ }
+ }
+
+ // When the theme is empty, it means that no theme was loaded and
+ // `@import "tailwindcss"`, `@reference "app.css"` or similar is
+ // very likely missing.
+ if (designSystem.theme.size === 0) {
+ throw new Error(
+ `Cannot apply unknown utility class \`${candidate}\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive`,
+ )
+ }
+
+ // Fallback to most generic error message
+ throw new Error(`Cannot apply unknown utility class \`${candidate}\``)
},
})
diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts
index ea64fe3a1239..196115d6357e 100644
--- a/packages/tailwindcss/src/compat/config.test.ts
+++ b/packages/tailwindcss/src/compat/config.test.ts
@@ -1223,6 +1223,7 @@ test('utilities used in @apply must be prefixed', async () => {
await expect(
compile(
css`
+ @tailwind utilities;
@config "./config.js";
.my-underline {
@@ -1238,7 +1239,7 @@ test('utilities used in @apply must be prefixed', async () => {
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Cannot apply unknown utility class: underline]`,
+ `[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`,
)
})
@@ -1440,7 +1441,7 @@ test('blocklisted candidates cannot be used with `@apply`', async () => {
},
),
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Cannot apply unknown utility class: bg-white]`,
+ `[Error: Cannot apply utility class \`bg-white\` because it has been explicitly disabled: https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-excluding-classes]`,
)
})
diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts
index 9c6df63fc3f6..e34faca42925 100644
--- a/packages/tailwindcss/src/index.test.ts
+++ b/packages/tailwindcss/src/index.test.ts
@@ -286,6 +286,88 @@ describe('@apply', () => {
)
})
+ it('@apply referencing theme values without `@tailwind utilities` or `@reference` should error', () => {
+ return expect(() =>
+ compileCss(css`
+ .foo {
+ @apply p-2;
+ }
+ `),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `[Error: Cannot apply unknown utility class \`p-2\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive]`,
+ )
+ })
+
+ it('@apply referencing theme values with `@tailwind utilities` should work', async () => {
+ return expect(
+ await compileCss(
+ css`
+ @import 'tailwindcss';
+
+ .foo {
+ @apply p-2;
+ }
+ `,
+ [],
+ {
+ async loadStylesheet() {
+ return {
+ path: '',
+ base: '/',
+ content: css`
+ @theme {
+ --spacing: 0.25rem;
+ }
+ @tailwind utilities;
+ `,
+ }
+ },
+ },
+ ),
+ ).toMatchInlineSnapshot(`
+ ":root, :host {
+ --spacing: .25rem;
+ }
+
+ .foo {
+ padding: calc(var(--spacing) * 2);
+ }"
+ `)
+ })
+
+ it('@apply referencing theme values with `@reference` should work', async () => {
+ return expect(
+ await compileCss(
+ css`
+ @reference "style.css";
+
+ .foo {
+ @apply p-2;
+ }
+ `,
+ [],
+ {
+ async loadStylesheet() {
+ return {
+ path: '',
+ base: '/',
+ content: css`
+ @theme {
+ --spacing: 0.25rem;
+ }
+ @tailwind utilities;
+ `,
+ }
+ },
+ },
+ ),
+ ).toMatchInlineSnapshot(`
+ ".foo {
+ padding: calc(var(--spacing, .25rem) * 2);
+ }"
+ `)
+ })
+
it('should replace @apply with the correct result', async () => {
expect(
await compileCss(css`
@@ -466,7 +548,7 @@ describe('@apply', () => {
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Cannot apply unknown utility class: bg-not-found]`,
+ `[Error: Cannot apply unknown utility class \`bg-not-found\`. Are you using CSS modules or similar and missing \`@reference\`? https://tailwindcss.com/docs/functions-and-directives#reference-directive]`,
)
})
@@ -474,13 +556,16 @@ describe('@apply', () => {
await expect(
compile(css`
@tailwind utilities;
+ @theme {
+ --color-red-500: red;
+ }
.foo {
- @apply hocus:bg-red-500;
+ @apply hocus:hover:pocus:bg-red-500;
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Cannot apply unknown utility class: hocus:bg-red-500]`,
+ `[Error: Cannot apply utility class \`hocus:hover:pocus:bg-red-500\` because the \`hocus\` and \`pocus\` variants do not exist.]`,
)
})
diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts
index 6e493c3689d0..4907306eee84 100644
--- a/packages/tailwindcss/src/index.ts
+++ b/packages/tailwindcss/src/index.ts
@@ -624,14 +624,6 @@ async function parseCss(
firstThemeRule.nodes = [context({ theme: true }, nodes)]
}
- // Replace the `@tailwind utilities` node with a context since it prints
- // children directly.
- if (utilitiesNode) {
- let node = utilitiesNode as AstNode as Context
- node.kind = 'context'
- node.context = {}
- }
-
// Replace the `@variant` at-rules with the actual variant rules.
if (variantNodes.length > 0) {
for (let variantNode of variantNodes) {
@@ -659,6 +651,14 @@ async function parseCss(
features |= substituteFunctions(ast, designSystem)
features |= substituteAtApply(ast, designSystem)
+ // Replace the `@tailwind utilities` node with a context since it prints
+ // children directly.
+ if (utilitiesNode) {
+ let node = utilitiesNode as AstNode as Context
+ node.kind = 'context'
+ node.context = {}
+ }
+
// Remove `@utility`, we couldn't replace it before yet because we had to
// handle the nested `@apply` at-rules first.
walk(ast, (node, { replaceWith }) => {
diff --git a/packages/tailwindcss/src/prefix.test.ts b/packages/tailwindcss/src/prefix.test.ts
index de426ab459fa..c60383de47e3 100644
--- a/packages/tailwindcss/src/prefix.test.ts
+++ b/packages/tailwindcss/src/prefix.test.ts
@@ -89,7 +89,7 @@ test('utilities used in @apply must be prefixed', async () => {
}
`),
).rejects.toThrowErrorMatchingInlineSnapshot(
- `[Error: Cannot apply unknown utility class: underline]`,
+ `[Error: Cannot apply unprefixed utility class \`underline\`. Did you mean \`tw:underline\`?]`,
)
})
diff --git a/packages/tailwindcss/src/theme.ts b/packages/tailwindcss/src/theme.ts
index 2928afd3bd55..dc9c750a12f8 100644
--- a/packages/tailwindcss/src/theme.ts
+++ b/packages/tailwindcss/src/theme.ts
@@ -51,6 +51,10 @@ export class Theme {
private keyframes = new Set([]),
) {}
+ get size() {
+ return this.values.size
+ }
+
add(key: string, value: string, options = ThemeOptions.NONE, src?: Declaration['src']): void {
if (key.endsWith('-*')) {
if (value !== 'initial') {