Skip to content

Commit

Permalink
implement @variant
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Jan 17, 2025
1 parent 988e90e commit 3ccb53d
Show file tree
Hide file tree
Showing 2 changed files with 251 additions and 1 deletion.
186 changes: 186 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3315,3 +3315,189 @@ 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 multiple `@variant` params at once', async () => {
await expect(
compileCss(
css`
.btn {
background: black;
@variant hover:focus {
background: white;
}
}
`,
[],
),
).resolves.toMatchInlineSnapshot(`
".btn {
background: #000;
}
@media (hover: hover) {
.btn:hover:focus {
background: #fff;
}
}"
`)
})

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:[&.foo] {
background: white;
}
}
`,
[],
),
).resolves.toMatchInlineSnapshot(`
".btn {
background: #000;
}
@container (width >= 768px) {
.btn.foo {
background: #fff;
}
}"
`)
})
})
66 changes: 65 additions & 1 deletion packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ 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'
Expand Down Expand Up @@ -97,6 +97,9 @@ export const enum Features {

// `@tailwind utilities` was used
Utilities = 1 << 4,

// `@variant` was used
Variants = 1 << 5,
}

async function parseCss(
Expand All @@ -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

Expand Down Expand Up @@ -213,6 +217,42 @@ async function parseCss(
return
}

// 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) {
Expand Down Expand Up @@ -501,6 +541,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)

for (let variant of segment(variantNode.params, ':').reverse()) {
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)

Expand Down

0 comments on commit 3ccb53d

Please sign in to comment.