diff --git a/integrations/postcss/multi-root.test.ts b/integrations/postcss/multi-root.test.ts index bfd2340e5017..327becdfef99 100644 --- a/integrations/postcss/multi-root.test.ts +++ b/integrations/postcss/multi-root.test.ts @@ -30,11 +30,11 @@ test( `, 'src/root1.css': css` @import './shared.css'; - @variant one (&:is([data-root='1'])); + @custom-variant one (&:is([data-root='1'])); `, 'src/root2.css': css` @import './shared.css'; - @variant two (&:is([data-root='2'])); + @custom-variant two (&:is([data-root='2'])); `, }, }, diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index e656253a9c77..7fb857b31aec 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -1433,7 +1433,7 @@ test( @tailwind components; @tailwind utilities; - @variant hocus (&:hover, &:focus); + @custom-variant hocus (&:hover, &:focus); @theme { --color-red-500: #f00; @@ -1539,7 +1539,7 @@ test( @config './tailwind.config.ts'; - @variant hocus (&:hover, &:focus); + @custom-variant hocus (&:hover, &:focus); @theme { --color-red-500: #f00; @@ -1675,7 +1675,7 @@ test( @tailwind components; @tailwind utilities; - @variant hocus (&:hover, &:focus); + @custom-variant hocus (&:hover, &:focus); @theme { --color-red-500: #f00; diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index 56c77189c53e..845402bd3055 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -167,7 +167,7 @@ test( @source '../node_modules/my-external-lib/**/*.{html}'; - @variant dark (&:where(.dark, .dark *)); + @custom-variant dark (&:where(.dark, .dark *)); @theme { --shadow-*: initial; diff --git a/integrations/vite/multi-root.test.ts b/integrations/vite/multi-root.test.ts index 39da43690448..b407f7da0bc4 100644 --- a/integrations/vite/multi-root.test.ts +++ b/integrations/vite/multi-root.test.ts @@ -48,7 +48,7 @@ test( `, 'src/root1.css': css` @import './shared.css'; - @variant one (&:is([data-root='1'])); + @custom-variant one (&:is([data-root='1'])); `, 'root2.html': html`
@@ -60,7 +60,7 @@ test( `, 'src/root2.css': css` @import './shared.css'; - @variant two (&:is([data-root='2'])); + @custom-variant two (&:is([data-root='2'])); `, }, }, @@ -124,7 +124,7 @@ test( `, 'src/root1.css': css` @import './shared.css'; - @variant one (&:is([data-root='1'])); + @custom-variant one (&:is([data-root='1'])); `, 'root2.html': html` @@ -136,7 +136,7 @@ test( `, 'src/root2.css': css` @import './shared.css'; - @variant two (&:is([data-root='2'])); + @custom-variant two (&:is([data-root='2'])); `, }, }, diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts index 7dad26e73ebc..268058cbc366 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts @@ -16,6 +16,7 @@ export function migrateMissingLayers(): Plugin { node.name === 'source' || node.name === 'theme' || node.name === 'utility' || + node.name === 'custom-variant' || node.name === 'variant' ) { if (bucket.length > 0) { diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-preflight.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-preflight.test.ts index 21793e1ed2c5..f9819f35ad7c 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-preflight.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-preflight.test.ts @@ -135,7 +135,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai await migrate(css` @import 'tailwindcss'; - @variant foo { + @custom-variant foo { } @utility bar { @@ -153,7 +153,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai ).toMatchInlineSnapshot(` "@import 'tailwindcss'; - @variant foo { + @custom-variant foo { } /* @@ -193,7 +193,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai await migrate(css` @import 'tailwindcss/preflight'; - @variant foo { + @custom-variant foo { } @utility bar { @@ -211,7 +211,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai ).toMatchInlineSnapshot(` "@import 'tailwindcss/preflight'; - @variant foo { + @custom-variant foo { } /* @@ -249,7 +249,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai it('should not add the backwards compatibility CSS when no `@import "tailwindcss"` or `@import "tailwindcss/preflight"` exists', async () => { expect( await migrate(css` - @variant foo { + @custom-variant foo { } @utility bar { @@ -265,7 +265,7 @@ it('should not add the backwards compatibility CSS when no `@import "tailwindcss } `), ).toMatchInlineSnapshot(` - "@variant foo { + "@custom-variant foo { } @utility bar { @@ -287,7 +287,7 @@ it('should not add the backwards compatibility CSS when another `@import "tailwi await migrate(css` @import 'tailwindcss/theme'; - @variant foo { + @custom-variant foo { } @utility bar { @@ -305,7 +305,7 @@ it('should not add the backwards compatibility CSS when another `@import "tailwi ).toMatchInlineSnapshot(` "@import 'tailwindcss/theme'; - @variant foo { + @custom-variant foo { } @utility bar { diff --git a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts index b99badeb01b3..f76a9424930f 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts @@ -10,7 +10,7 @@ const BUCKET_ORDER = [ 'config', // @config 'plugin', // @plugin 'source', // @source - 'variant', // @variant + 'custom-variant', // @custom-variant 'theme', // @theme // Styles @@ -75,7 +75,7 @@ export function sortBuckets(): Plugin { // Known at-rules else if ( node.type === 'atrule' && - ['config', 'plugin', 'source', 'theme', 'utility', 'variant'].includes(node.name) + ['config', 'plugin', 'source', 'theme', 'utility', 'custom-variant'].includes(node.name) ) { injectInto(node.name, node) } diff --git a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts index 6a8a7f4010a7..c05ee6babebe 100644 --- a/packages/@tailwindcss-upgrade/src/migrate-js-config.ts +++ b/packages/@tailwindcss-upgrade/src/migrate-js-config.ts @@ -204,7 +204,7 @@ function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string { if (variant === '') { return '' } - return `\n@tw-bucket variant {\n@variant dark (${variant});\n}\n` + return `\n@tw-bucket custom-variant {\n@custom-variant dark (${variant});\n}\n` } // Returns a string identifier used to section theme declarations diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts index e42595645b45..ffceaaecb0da 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts @@ -60,19 +60,19 @@ test('it works with custom variants', async () => { let designSystem = await __unstable__loadDesignSystem( css` @import 'tailwindcss'; - @variant atrule { + @custom-variant atrule { @media (print) { @slot; } } - @variant combinator { + @custom-variant combinator { > * { @slot; } } - @variant pseudo { + @custom-variant pseudo { &::before { @slot; } diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index ae52098be974..6052724f33b9 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -256,11 +256,23 @@ export function optimizeAst(ast: AstNode[]) { // Rule else if (node.kind === 'rule') { - let copy = { ...node, nodes: [] } - for (let child of node.nodes) { - transform(child, copy.nodes, depth + 1) + // Rules with `&` as the selector should be flattened + if (node.selector === '&') { + for (let child of node.nodes) { + let nodes: AstNode[] = [] + transform(child, nodes, depth + 1) + parent.push(...nodes) + } + } + + // + else { + let copy = { ...node, nodes: [] } + for (let child of node.nodes) { + transform(child, copy.nodes, depth + 1) + } + parent.push(copy) } - parent.push(copy) } // AtRule `@property` diff --git a/packages/tailwindcss/src/compat/config.test.ts b/packages/tailwindcss/src/compat/config.test.ts index ff21abc552a8..7523b2f3410c 100644 --- a/packages/tailwindcss/src/compat/config.test.ts +++ b/packages/tailwindcss/src/compat/config.test.ts @@ -216,8 +216,8 @@ test('Variants in CSS overwrite variants from plugins', async () => { let input = css` @tailwind utilities; @config "./config.js"; - @variant dark (&:is(.my-dark)); - @variant light (&:is(.my-light)); + @custom-variant dark (&:is(.my-dark)); + @custom-variant light (&:is(.my-light)); ` let compiler = await compile(input, { diff --git a/packages/tailwindcss/src/compat/plugin-api.ts b/packages/tailwindcss/src/compat/plugin-api.ts index e746b08468fc..13673280769b 100644 --- a/packages/tailwindcss/src/compat/plugin-api.ts +++ b/packages/tailwindcss/src/compat/plugin-api.ts @@ -10,7 +10,7 @@ import { DefaultMap } from '../utils/default-map' import { inferDataType } from '../utils/infer-data-type' import { segment } from '../utils/segment' import { toKeyPath } from '../utils/to-key-path' -import { compoundsForSelectors, substituteAtSlot } from '../variants' +import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtSlot } from '../variants' import type { ResolvedConfig, UserConfig } from './config/types' import { createThemeFn } from './plugin-functions' import * as SelectorParser from './selector-parser' @@ -108,6 +108,12 @@ export function buildPluginApi({ }, addVariant(name, variant) { + if (!IS_VALID_VARIANT_NAME.test(name)) { + throw new Error( + `\`addVariant('${name}')\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.`, + ) + } + // Single selector or multiple parallel selectors if (typeof variant === 'string' || Array.isArray(variant)) { designSystem.variants.static( diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index f34949ccaaf7..0b5851b70c8b 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -2493,56 +2493,66 @@ describe('@source', () => { }) }) -describe('@variant', () => { - test('@variant must be top-level and cannot be nested', () => { +describe('@custom-variant', () => { + test('@custom-variant must be top-level and cannot be nested', () => { + return expect( + compileCss(css` + @custom-variant foo:bar (&:hover, &:focus); + `), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: \`@custom-variant foo:bar\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.]`, + ) + }) + + test('@custom-variant must not container special characters', () => { return expect( compileCss(css` .foo { - @variant hocus (&:hover, &:focus); + @custom-variant foo:bar (&:hover, &:focus); } `), - ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@variant\` cannot be nested.]`) + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: \`@custom-variant\` cannot be nested.]`) }) - test('@variant with no body must include a selector', () => { + test('@custom-variant with no body must include a selector', () => { return expect( compileCss(css` - @variant hocus; + @custom-variant hocus; `), ).rejects.toThrowErrorMatchingInlineSnapshot( - '[Error: `@variant hocus` has no selector or body.]', + '[Error: `@custom-variant hocus` has no selector or body.]', ) }) - test('@variant with selector must include a body', () => { + test('@custom-variant with selector must include a body', () => { return expect( compileCss(css` - @variant hocus { + @custom-variant hocus { } `), ).rejects.toThrowErrorMatchingInlineSnapshot( - '[Error: `@variant hocus` has no selector or body.]', + '[Error: `@custom-variant hocus` has no selector or body.]', ) }) - test('@variant cannot have both a selector and a body', () => { + test('@custom-variant cannot have both a selector and a body', () => { return expect( compileCss(css` - @variant hocus (&:hover, &:focus) { + @custom-variant hocus (&:hover, &:focus) { &:is(.potato) { @slot; } } `), ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: \`@variant hocus\` cannot have both a selector and a body.]`, + `[Error: \`@custom-variant hocus\` cannot have both a selector and a body.]`, ) }) describe('body-less syntax', () => { test('selector variant', async () => { let { build } = await compile(css` - @variant hocus (&:hover, &:focus); + @custom-variant hocus (&:hover, &:focus); @layer utilities { @tailwind utilities; @@ -2565,7 +2575,7 @@ describe('@variant', () => { test('at-rule variant', async () => { let { build } = await compile(css` - @variant any-hover (@media (any-hover: hover)); + @custom-variant any-hover (@media (any-hover: hover)); @layer utilities { @tailwind utilities; @@ -2588,7 +2598,7 @@ describe('@variant', () => { test('style-rules and at-rules', async () => { let { build } = await compile(css` - @variant cant-hover (&:not(:hover), &:not(:active), @media not (any-hover: hover), @media not (pointer: fine)); + @custom-variant cant-hover (&:not(:hover), &:not(:active), @media not (any-hover: hover), @media not (pointer: fine)); @layer utilities { @tailwind utilities; @@ -2621,7 +2631,7 @@ describe('@variant', () => { describe('body with @slot syntax', () => { test('selector with @slot', async () => { let { build } = await compile(css` - @variant selected { + @custom-variant selected { &[data-selected] { @slot; } @@ -2644,7 +2654,7 @@ describe('@variant', () => { test('grouped selectors with @slot', async () => { let { build } = await compile(css` - @variant hocus { + @custom-variant hocus { &:hover, &:focus { @slot; @@ -2668,7 +2678,7 @@ describe('@variant', () => { test('multiple selectors with @slot', async () => { let { build } = await compile(css` - @variant hocus { + @custom-variant hocus { &:hover { @slot; } @@ -2695,7 +2705,7 @@ describe('@variant', () => { test('nested selector with @slot', async () => { let { build } = await compile(css` - @variant custom-before { + @custom-variant custom-before { & { --has-before: 1; &::before { @@ -2725,7 +2735,7 @@ describe('@variant', () => { test('grouped nested selectors with @slot', async () => { let { build } = await compile(css` - @variant custom-before { + @custom-variant custom-before { & { --has-before: 1; &::before { @@ -2758,7 +2768,7 @@ describe('@variant', () => { test('nested multiple selectors with @slot', async () => { let { build } = await compile(css` - @variant hocus { + @custom-variant hocus { &:hover { @media (hover: hover) { @slot; @@ -2803,7 +2813,7 @@ describe('@variant', () => { test('selector nested under at-rule with @slot', async () => { let { build } = await compile(css` - @variant hocus { + @custom-variant hocus { @media (hover: hover) { &:hover { @slot; @@ -2830,7 +2840,7 @@ describe('@variant', () => { test('at-rule with @slot', async () => { let { build } = await compile(css` - @variant any-hover { + @custom-variant any-hover { @media (any-hover: hover) { @slot; } @@ -2855,7 +2865,7 @@ describe('@variant', () => { test('multiple at-rules with @slot', async () => { let { build } = await compile(css` - @variant desktop { + @custom-variant desktop { @media (any-hover: hover) { @slot; } @@ -2890,7 +2900,7 @@ describe('@variant', () => { test('nested at-rules with @slot', async () => { let { build } = await compile(css` - @variant custom-variant { + @custom-variant custom-variant { @media (orientation: landscape) { @media screen { @slot; @@ -2929,7 +2939,7 @@ describe('@variant', () => { test('at-rule and selector with @slot', async () => { let { build } = await compile(css` - @variant custom-dark { + @custom-variant custom-dark { @media (prefers-color-scheme: dark) { @slot; } @@ -2964,7 +2974,7 @@ describe('@variant', () => { expect( await compileCss( css` - @variant dark (&:is([data-theme='dark'] *)); + @custom-variant dark (&:is([data-theme='dark'] *)); @layer utilities { @tailwind utilities; } @@ -2993,7 +3003,7 @@ describe('@variant', () => { expect( await compileCss( css` - @variant foo (@media foo); + @custom-variant foo (@media foo); @layer utilities { @tailwind utilities; @@ -3137,7 +3147,7 @@ describe('`@import "…" reference`', () => { @theme { --breakpoint-md: 768px; } - @variant hocus (&:hover, &:focus); + @custom-variant hocus (&:hover, &:focus); `, base: '/root/foo', } @@ -3217,7 +3227,7 @@ describe('`@import "…" reference`', () => { @utility foo { color: red; } - @variant hocus (&:hover, &:focus); + @custom-variant hocus (&:hover, &:focus); } .bar { @@ -3315,3 +3325,164 @@ describe('`@import "…" reference`', () => { `) }) }) + +describe('@variant', () => { + it('should convert legacy body-less `@variant` as a `@custom-variant`', async () => { + await expect( + compileCss( + css` + @variant hocus (&:hover, &:focus); + @tailwind utilities; + `, + ['hocus:underline'], + ), + ).resolves.toMatchInlineSnapshot(` + ".hocus\\:underline:hover, .hocus\\:underline:focus { + text-decoration-line: underline; + }" + `) + }) + + it('should convert legacy `@variant` with `@slot` as a `@custom-variant`', async () => { + await expect( + compileCss( + css` + @variant hocus { + &:hover { + @slot; + } + + &:focus { + @slot; + } + } + @tailwind utilities; + `, + ['hocus:underline'], + ), + ).resolves.toMatchInlineSnapshot(` + ".hocus\\:underline:hover, .hocus\\:underline:focus { + text-decoration-line: underline; + }" + `) + }) + + it('should be possible to use `@variant` in your CSS', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant dark { + background: white; + } + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @media (prefers-color-scheme: dark) { + .btn { + background: #fff; + } + }" + `) + }) + + it('should be possible to use `@variant` in your CSS with a `@custom-variant` that is defined later', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant hocus { + background: white; + } + } + + @custom-variant hocus (&:hover, &:focus); + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + .btn:hover, .btn:focus { + background: #fff; + }" + `) + }) + + it('should be possible to use nested `@variant` rules', async () => { + await expect( + compileCss( + css` + .btn { + background: black; + + @variant disabled { + @variant focus { + background: white; + } + } + } + @tailwind utilities; + `, + ['disabled:focus:underline'], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + .btn:disabled:focus { + background: #fff; + } + + .disabled\\:focus\\:underline:disabled:focus { + text-decoration-line: underline; + }" + `) + }) + + it('should be possible to use `@variant` with a funky looking variants', async () => { + await expect( + compileCss( + css` + @theme inline reference { + --container-md: 768px; + } + + .btn { + background: black; + + @variant @md { + @variant [&.foo] { + background: white; + } + } + } + `, + [], + ), + ).resolves.toMatchInlineSnapshot(` + ".btn { + background: #000; + } + + @container (width >= 768px) { + .btn.foo { + background: #fff; + } + }" + `) + }) +}) diff --git a/packages/tailwindcss/src/index.ts b/packages/tailwindcss/src/index.ts index 856240ce2cec..3e4bc5a3cce0 100644 --- a/packages/tailwindcss/src/index.ts +++ b/packages/tailwindcss/src/index.ts @@ -21,14 +21,14 @@ import { substituteAtImports } from './at-import' import { applyCompatibilityHooks } from './compat/apply-compat-hooks' import type { UserConfig } from './compat/config/types' import { type Plugin } from './compat/plugin-api' -import { compileCandidates } from './compile' +import { applyVariant, compileCandidates } from './compile' import { substituteFunctions } from './css-functions' import * as CSS from './css-parser' import { buildDesignSystem, type DesignSystem } from './design-system' import { Theme, ThemeOptions } from './theme' import { createCssUtility } from './utilities' import { segment } from './utils/segment' -import { compoundsForSelectors } from './variants' +import { compoundsForSelectors, IS_VALID_VARIANT_NAME } from './variants' export type Config = UserConfig const IS_VALID_PREFIX = /^[a-z]+$/ @@ -97,6 +97,9 @@ export const enum Features { // `@tailwind utilities` was used Utilities = 1 << 4, + + // `@variant` was used + Variants = 1 << 5, } async function parseCss( @@ -118,6 +121,7 @@ async function parseCss( let customUtilities: ((designSystem: DesignSystem) => void)[] = [] let firstThemeRule = null as StyleRule | null let utilitiesNode = null as AtRule | null + let variantNodes: AtRule[] = [] let globs: { base: string; pattern: string }[] = [] let root = null as Root @@ -213,25 +217,67 @@ async function parseCss( return } - // Register custom variants from `@variant` at-rules + // Apply `@variant` at-rules if (node.name === '@variant') { + // Legacy `@variant` at-rules containing `@slot` or without a body should + // be considered a `@custom-variant` at-rule. + if (parent === null) { + // Body-less `@variant`, e.g.: `@variant foo (…);` + if (node.nodes.length === 0) { + node.name = '@custom-variant' + } + + // Using `@slot`: + // + // ```css + // @variant foo { + // &:hover { + // @slot; + // } + // } + // ``` + else { + walk(node.nodes, (child) => { + if (child.kind === 'at-rule' && child.name === '@slot') { + node.name = '@custom-variant' + return WalkAction.Stop + } + }) + } + } + + // Collect all the `@variant` at-rules, we will replace them later once + // all variants are registered in the system. + else { + variantNodes.push(node) + } + } + + // Register custom variants from `@custom-variant` at-rules + if (node.name === '@custom-variant') { if (parent !== null) { - throw new Error('`@variant` cannot be nested.') + throw new Error('`@custom-variant` cannot be nested.') } - // Remove `@variant` at-rule so it's not included in the compiled CSS + // Remove `@custom-variant` at-rule so it's not included in the compiled CSS replaceWith([]) let [name, selector] = segment(node.params, ' ') + if (!IS_VALID_VARIANT_NAME.test(name)) { + throw new Error( + `\`@custom-variant ${name}\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.`, + ) + } + if (node.nodes.length > 0 && selector) { - throw new Error(`\`@variant ${name}\` cannot have both a selector and a body.`) + throw new Error(`\`@custom-variant ${name}\` cannot have both a selector and a body.`) } - // Variants with a selector, but without a body, e.g.: `@variant hocus (&:hover, &:focus);` + // Variants with a selector, but without a body, e.g.: `@custom-variant hocus (&:hover, &:focus);` if (node.nodes.length === 0) { if (!selector) { - throw new Error(`\`@variant ${name}\` has no selector or body.`) + throw new Error(`\`@custom-variant ${name}\` has no selector or body.`) } let selectors = segment(selector.slice(1, -1), ',') @@ -279,7 +325,7 @@ async function parseCss( // E.g.: // // ```css - // @variant hocus { + // @custom-variant hocus { // &:hover { // @slot; // } @@ -501,6 +547,30 @@ async function parseCss( node.context = {} } + // Replace the `@variant` at-rules with the actual variant rules. + if (variantNodes.length > 0) { + for (let variantNode of variantNodes) { + // Starting with the `&` rule node + let node = styleRule('&', variantNode.nodes) + + let variant = variantNode.params + + let variantAst = designSystem.parseVariant(variant) + if (variantAst === null) { + throw new Error(`Cannot use \`@variant\` with unknown variant: ${variant}`) + } + + let result = applyVariant(node, variantAst, designSystem.variants) + if (result === null) { + throw new Error(`Cannot use \`@variant\` with variant: ${variant}`) + } + + // Update the variant at-rule node, to be the `&` rule node + Object.assign(variantNode, node) + } + features |= Features.Variants + } + features |= substituteFunctions(ast, designSystem) features |= substituteAtApply(ast, designSystem) diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts index abf4dc9bbb6e..48a30a80d7b4 100644 --- a/packages/tailwindcss/src/intellisense.test.ts +++ b/packages/tailwindcss/src/intellisense.test.ts @@ -82,7 +82,7 @@ test('getVariants compound', () => { test('variant selectors are in the correct order', async () => { let input = css` - @variant overactive { + @custom-variant overactive { &:hover { @media (hover: hover) { &:focus { @@ -386,8 +386,8 @@ test('Functional utilities from plugins are listed in hovers and completions', a test('Custom at-rule variants do not show up as a value under `group`', async () => { let input = css` @import 'tailwindcss/utilities'; - @variant variant-1 (@media foo); - @variant variant-2 { + @custom-variant variant-1 (@media foo); + @custom-variant variant-2 { @media bar { @slot; } diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 8ee60ec80ab1..5f4b9d106aa2 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -518,7 +518,7 @@ test('group-*', async () => { expect( await compileCss( css` - @variant hocus { + @custom-variant hocus { &:hover, &:focus { @slot; @@ -560,8 +560,8 @@ test('group-*', async () => { expect( await compileCss( css` - @variant custom-at-rule (@media foo); - @variant nested-selectors { + @custom-variant custom-at-rule (@media foo); + @custom-variant nested-selectors { &:hover { &:focus { @slot; @@ -610,7 +610,7 @@ test('peer-*', async () => { expect( await compileCss( css` - @variant hocus { + @custom-variant hocus { &:hover, &:focus { @slot; @@ -651,8 +651,8 @@ test('peer-*', async () => { expect( await compileCss( css` - @variant custom-at-rule (@media foo); - @variant nested-selectors { + @custom-variant custom-at-rule (@media foo); + @custom-variant nested-selectors { &:hover { &:focus { @slot; @@ -1361,14 +1361,14 @@ test('not', async () => { expect( await compileCss( css` - @variant hocus { + @custom-variant hocus { &:hover, &:focus { @slot; } } - @variant device-hocus { + @custom-variant device-hocus { @media (hover: hover) { &:hover, &:focus { @@ -1625,19 +1625,19 @@ test('not', async () => { expect( await compileCss( css` - @variant nested-at-rules { + @custom-variant nested-at-rules { @media foo { @media bar { @slot; } } } - @variant multiple-media-conditions { + @custom-variant multiple-media-conditions { @media foo, bar { @slot; } } - @variant nested-style-rules { + @custom-variant nested-style-rules { &:hover { &:focus { @slot; @@ -1706,7 +1706,7 @@ test('has', async () => { expect( await compileCss( css` - @variant hocus { + @custom-variant hocus { &:hover, &:focus { @slot; @@ -1759,8 +1759,8 @@ test('has', async () => { expect( await compileCss( css` - @variant custom-at-rule (@media foo); - @variant nested-selectors { + @custom-variant custom-at-rule (@media foo); + @custom-variant nested-selectors { &:hover { &:focus { @slot; diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 99cd20a455d4..d7a1ab5b1390 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -18,6 +18,8 @@ import { DefaultMap } from './utils/default-map' import { isPositiveInteger } from './utils/infer-data-type' import { segment } from './utils/segment' +export const IS_VALID_VARIANT_NAME = /^@?[a-zA-Z0-9_-]*$/ + type VariantFn