Skip to content

Commit

Permalink
Implement @variant (#15663)
Browse files Browse the repository at this point in the history
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;
    }
  }
}
```
  • Loading branch information
RobinMalfait authored Jan 21, 2025
1 parent 67237e2 commit 4035ab0
Show file tree
Hide file tree
Showing 17 changed files with 350 additions and 88 deletions.
4 changes: 2 additions & 2 deletions integrations/postcss/multi-root.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']));
`,
},
},
Expand Down
6 changes: 3 additions & 3 deletions integrations/upgrade/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1433,7 +1433,7 @@ test(
@tailwind components;
@tailwind utilities;
@variant hocus (&:hover, &:focus);
@custom-variant hocus (&:hover, &:focus);
@theme {
--color-red-500: #f00;
Expand Down Expand Up @@ -1539,7 +1539,7 @@ test(
@config './tailwind.config.ts';
@variant hocus (&:hover, &:focus);
@custom-variant hocus (&:hover, &:focus);
@theme {
--color-red-500: #f00;
Expand Down Expand Up @@ -1675,7 +1675,7 @@ test(
@tailwind components;
@tailwind utilities;
@variant hocus (&:hover, &:focus);
@custom-variant hocus (&:hover, &:focus);
@theme {
--color-red-500: #f00;
Expand Down
2 changes: 1 addition & 1 deletion integrations/upgrade/js-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions integrations/vite/multi-root.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<head>
Expand All @@ -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']));
`,
},
},
Expand Down Expand Up @@ -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`
<head>
Expand All @@ -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']));
`,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
}
/*
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
}
/*
Expand Down Expand Up @@ -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 {
Expand All @@ -265,7 +265,7 @@ it('should not add the backwards compatibility CSS when no `@import "tailwindcss
}
`),
).toMatchInlineSnapshot(`
"@variant foo {
"@custom-variant foo {
}
@utility bar {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/@tailwindcss-upgrade/src/codemods/sort-buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const BUCKET_ORDER = [
'config', // @config
'plugin', // @plugin
'source', // @source
'variant', // @variant
'custom-variant', // @custom-variant
'theme', // @theme

// Styles
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-upgrade/src/migrate-js-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
20 changes: 16 additions & 4 deletions packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
8 changes: 7 additions & 1 deletion packages/tailwindcss/src/compat/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 4035ab0

Please sign in to comment.