Skip to content

Commit 4035ab0

Browse files
authored
Implement @variant (#15663)
This PR replaces `@variant` with `@custom-variant` for registering custom variants via your CSS. In addition, this PR introduces `@variant` that can be used in your CSS to use a variant while writing custom CSS. E.g.: ```css .btn { background: white; @variant dark { background: black; } } ``` Compiles to: ```css .btn { background: white; } @media (prefers-color-scheme: dark) { .btn { background: black; } } ``` For backwards compatibility, the `@variant` rules that don't have a body and are defined inline: ```css @variant hocus (&:hover, &:focus); ``` And `@variant` rules that are defined with a body and a `@slot`: ```css @variant hocus { &:hover, &:focus { @slot; } } ``` Will automatically be upgraded to `@custom-variant` internally, so no breaking changes are introduced with this PR. --- TODO: - [x] ~~Decide whether we want to allow multiple variants and if so, what syntax should be used. If not, nesting `@variant <variant> {}` will be the way to go.~~ Only a single `@variant <variant>` can be used, if you want to use multiple, nesting should be used: ```css .foo { @variant hover { @variant focus { color: red; } } } ```
1 parent 67237e2 commit 4035ab0

File tree

17 files changed

+350
-88
lines changed

17 files changed

+350
-88
lines changed

integrations/postcss/multi-root.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ test(
3030
`,
3131
'src/root1.css': css`
3232
@import './shared.css';
33-
@variant one (&:is([data-root='1']));
33+
@custom-variant one (&:is([data-root='1']));
3434
`,
3535
'src/root2.css': css`
3636
@import './shared.css';
37-
@variant two (&:is([data-root='2']));
37+
@custom-variant two (&:is([data-root='2']));
3838
`,
3939
},
4040
},

integrations/upgrade/index.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1433,7 +1433,7 @@ test(
14331433
@tailwind components;
14341434
@tailwind utilities;
14351435
1436-
@variant hocus (&:hover, &:focus);
1436+
@custom-variant hocus (&:hover, &:focus);
14371437
14381438
@theme {
14391439
--color-red-500: #f00;
@@ -1539,7 +1539,7 @@ test(
15391539
15401540
@config './tailwind.config.ts';
15411541
1542-
@variant hocus (&:hover, &:focus);
1542+
@custom-variant hocus (&:hover, &:focus);
15431543
15441544
@theme {
15451545
--color-red-500: #f00;
@@ -1675,7 +1675,7 @@ test(
16751675
@tailwind components;
16761676
@tailwind utilities;
16771677
1678-
@variant hocus (&:hover, &:focus);
1678+
@custom-variant hocus (&:hover, &:focus);
16791679
16801680
@theme {
16811681
--color-red-500: #f00;

integrations/upgrade/js-config.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ test(
167167
168168
@source '../node_modules/my-external-lib/**/*.{html}';
169169
170-
@variant dark (&:where(.dark, .dark *));
170+
@custom-variant dark (&:where(.dark, .dark *));
171171
172172
@theme {
173173
--shadow-*: initial;

integrations/vite/multi-root.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ test(
4848
`,
4949
'src/root1.css': css`
5050
@import './shared.css';
51-
@variant one (&:is([data-root='1']));
51+
@custom-variant one (&:is([data-root='1']));
5252
`,
5353
'root2.html': html`
5454
<head>
@@ -60,7 +60,7 @@ test(
6060
`,
6161
'src/root2.css': css`
6262
@import './shared.css';
63-
@variant two (&:is([data-root='2']));
63+
@custom-variant two (&:is([data-root='2']));
6464
`,
6565
},
6666
},
@@ -124,7 +124,7 @@ test(
124124
`,
125125
'src/root1.css': css`
126126
@import './shared.css';
127-
@variant one (&:is([data-root='1']));
127+
@custom-variant one (&:is([data-root='1']));
128128
`,
129129
'root2.html': html`
130130
<head>
@@ -136,7 +136,7 @@ test(
136136
`,
137137
'src/root2.css': css`
138138
@import './shared.css';
139-
@variant two (&:is([data-root='2']));
139+
@custom-variant two (&:is([data-root='2']));
140140
`,
141141
},
142142
},

packages/@tailwindcss-upgrade/src/codemods/migrate-missing-layers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function migrateMissingLayers(): Plugin {
1616
node.name === 'source' ||
1717
node.name === 'theme' ||
1818
node.name === 'utility' ||
19+
node.name === 'custom-variant' ||
1920
node.name === 'variant'
2021
) {
2122
if (bucket.length > 0) {

packages/@tailwindcss-upgrade/src/codemods/migrate-preflight.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai
135135
await migrate(css`
136136
@import 'tailwindcss';
137137
138-
@variant foo {
138+
@custom-variant foo {
139139
}
140140
141141
@utility bar {
@@ -153,7 +153,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai
153153
).toMatchInlineSnapshot(`
154154
"@import 'tailwindcss';
155155
156-
@variant foo {
156+
@custom-variant foo {
157157
}
158158
159159
/*
@@ -193,7 +193,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai
193193
await migrate(css`
194194
@import 'tailwindcss/preflight';
195195
196-
@variant foo {
196+
@custom-variant foo {
197197
}
198198
199199
@utility bar {
@@ -211,7 +211,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai
211211
).toMatchInlineSnapshot(`
212212
"@import 'tailwindcss/preflight';
213213
214-
@variant foo {
214+
@custom-variant foo {
215215
}
216216
217217
/*
@@ -249,7 +249,7 @@ it('should add the compatibility CSS before the first `@layer base` (if the "tai
249249
it('should not add the backwards compatibility CSS when no `@import "tailwindcss"` or `@import "tailwindcss/preflight"` exists', async () => {
250250
expect(
251251
await migrate(css`
252-
@variant foo {
252+
@custom-variant foo {
253253
}
254254
255255
@utility bar {
@@ -265,7 +265,7 @@ it('should not add the backwards compatibility CSS when no `@import "tailwindcss
265265
}
266266
`),
267267
).toMatchInlineSnapshot(`
268-
"@variant foo {
268+
"@custom-variant foo {
269269
}
270270
271271
@utility bar {
@@ -287,7 +287,7 @@ it('should not add the backwards compatibility CSS when another `@import "tailwi
287287
await migrate(css`
288288
@import 'tailwindcss/theme';
289289
290-
@variant foo {
290+
@custom-variant foo {
291291
}
292292
293293
@utility bar {
@@ -305,7 +305,7 @@ it('should not add the backwards compatibility CSS when another `@import "tailwi
305305
).toMatchInlineSnapshot(`
306306
"@import 'tailwindcss/theme';
307307
308-
@variant foo {
308+
@custom-variant foo {
309309
}
310310
311311
@utility bar {

packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const BUCKET_ORDER = [
1010
'config', // @config
1111
'plugin', // @plugin
1212
'source', // @source
13-
'variant', // @variant
13+
'custom-variant', // @custom-variant
1414
'theme', // @theme
1515

1616
// Styles
@@ -75,7 +75,7 @@ export function sortBuckets(): Plugin {
7575
// Known at-rules
7676
else if (
7777
node.type === 'atrule' &&
78-
['config', 'plugin', 'source', 'theme', 'utility', 'variant'].includes(node.name)
78+
['config', 'plugin', 'source', 'theme', 'utility', 'custom-variant'].includes(node.name)
7979
) {
8080
injectInto(node.name, node)
8181
}

packages/@tailwindcss-upgrade/src/migrate-js-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ function migrateDarkMode(unresolvedConfig: Config & { darkMode: any }): string {
204204
if (variant === '') {
205205
return ''
206206
}
207-
return `\n@tw-bucket variant {\n@variant dark (${variant});\n}\n`
207+
return `\n@tw-bucket custom-variant {\n@custom-variant dark (${variant});\n}\n`
208208
}
209209

210210
// Returns a string identifier used to section theme declarations

packages/@tailwindcss-upgrade/src/template/codemods/variant-order.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,19 @@ test('it works with custom variants', async () => {
6060
let designSystem = await __unstable__loadDesignSystem(
6161
css`
6262
@import 'tailwindcss';
63-
@variant atrule {
63+
@custom-variant atrule {
6464
@media (print) {
6565
@slot;
6666
}
6767
}
6868
69-
@variant combinator {
69+
@custom-variant combinator {
7070
> * {
7171
@slot;
7272
}
7373
}
7474
75-
@variant pseudo {
75+
@custom-variant pseudo {
7676
&::before {
7777
@slot;
7878
}

packages/tailwindcss/src/ast.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,23 @@ export function optimizeAst(ast: AstNode[]) {
256256

257257
// Rule
258258
else if (node.kind === 'rule') {
259-
let copy = { ...node, nodes: [] }
260-
for (let child of node.nodes) {
261-
transform(child, copy.nodes, depth + 1)
259+
// Rules with `&` as the selector should be flattened
260+
if (node.selector === '&') {
261+
for (let child of node.nodes) {
262+
let nodes: AstNode[] = []
263+
transform(child, nodes, depth + 1)
264+
parent.push(...nodes)
265+
}
266+
}
267+
268+
//
269+
else {
270+
let copy = { ...node, nodes: [] }
271+
for (let child of node.nodes) {
272+
transform(child, copy.nodes, depth + 1)
273+
}
274+
parent.push(copy)
262275
}
263-
parent.push(copy)
264276
}
265277

266278
// AtRule `@property`

packages/tailwindcss/src/compat/config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,8 @@ test('Variants in CSS overwrite variants from plugins', async () => {
216216
let input = css`
217217
@tailwind utilities;
218218
@config "./config.js";
219-
@variant dark (&:is(.my-dark));
220-
@variant light (&:is(.my-light));
219+
@custom-variant dark (&:is(.my-dark));
220+
@custom-variant light (&:is(.my-light));
221221
`
222222

223223
let compiler = await compile(input, {

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { DefaultMap } from '../utils/default-map'
1010
import { inferDataType } from '../utils/infer-data-type'
1111
import { segment } from '../utils/segment'
1212
import { toKeyPath } from '../utils/to-key-path'
13-
import { compoundsForSelectors, substituteAtSlot } from '../variants'
13+
import { compoundsForSelectors, IS_VALID_VARIANT_NAME, substituteAtSlot } from '../variants'
1414
import type { ResolvedConfig, UserConfig } from './config/types'
1515
import { createThemeFn } from './plugin-functions'
1616
import * as SelectorParser from './selector-parser'
@@ -108,6 +108,12 @@ export function buildPluginApi({
108108
},
109109

110110
addVariant(name, variant) {
111+
if (!IS_VALID_VARIANT_NAME.test(name)) {
112+
throw new Error(
113+
`\`addVariant('${name}')\` defines an invalid variant name. Variants should only contain alphanumeric, dashes or underscore characters.`,
114+
)
115+
}
116+
111117
// Single selector or multiple parallel selectors
112118
if (typeof variant === 'string' || Array.isArray(variant)) {
113119
designSystem.variants.static(

0 commit comments

Comments
 (0)