Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom variants via CSS #13992

Merged
merged 25 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0ee38a8
implement `@variant` in CSS
RobinMalfait Jul 11, 2024
73d689d
implement `addVariant(name, objectTree)`
RobinMalfait Jul 11, 2024
d0d6fd4
update changelog
RobinMalfait Jul 11, 2024
0bc2bbf
ensure that `@variant` can only be used top-level
RobinMalfait Jul 11, 2024
e17d037
simplify Plugin API type
RobinMalfait Jul 11, 2024
962cb7d
Use type instead of interface (for now)
adamwathan Jul 16, 2024
df44b71
Use more realistic variant for test
adamwathan Jul 16, 2024
743c050
Allow custom properties to use `@slot` as content
adamwathan Jul 16, 2024
f4065f1
Use "cannot" instead of "can not"
adamwathan Jul 16, 2024
884e01b
Remove `@variant` right away
adamwathan Jul 16, 2024
fee34b0
Throw when `@variant` is missing a selector or body
adamwathan Jul 16, 2024
ed77cc9
Use "CSS-in-JS" terminology instead of "CSS Tree"
adamwathan Jul 16, 2024
c1222f8
Rename tests
adamwathan Jul 16, 2024
72b334b
Mark some tests that seem wrong
adamwathan Jul 16, 2024
12f176b
Tweak comment, remove unnecessary return
adamwathan Jul 16, 2024
d424c1f
Ensure group is usable with custom selector lists
thecrypticace Jul 16, 2024
8e6e948
Only apply extra `:is(…)` when there are multiple selectors
thecrypticace Jul 16, 2024
e8d625c
Tweak comment
thecrypticace Jul 16, 2024
a4707e6
Throw when @variant has both selector and body
adamwathan Jul 16, 2024
48ccf46
Rework tests to use more realistic examples
adamwathan Jul 16, 2024
aaae982
Compound variants on an isolated copy
thecrypticace Jul 16, 2024
c6600ea
Handle selector lists for peer variants
thecrypticace Jul 16, 2024
63efa4b
Ignore at rules when compounding group and peer variants
thecrypticace Jul 16, 2024
e76038c
Re-enable skipped tests
thecrypticace Jul 16, 2024
e6c16e9
Update changelog
thecrypticace Jul 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add support for basic `addVariant` plugins with new `@plugin` directive ([#13982](https://github.com/tailwindlabs/tailwindcss/pull/13982))
- Add `@variant` at-rule for defining custom variants in CSS ([#13992](https://github.com/tailwindlabs/tailwindcss/pull/13992))

## [4.0.0-alpha.17] - 2024-07-04

Expand Down
25 changes: 24 additions & 1 deletion packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ export function comment(value: string): Comment {
}
}

export type CssInJs = { [key: string]: string | CssInJs }

export function objectToAst(obj: CssInJs): AstNode[] {
let ast: AstNode[] = []

for (let [name, value] of Object.entries(obj)) {
if (typeof value === 'string') {
if (!name.startsWith('--') && value === '@slot') {
ast.push(rule(name, [rule('@slot', [])]))
} else {
ast.push(decl(name, value))
}
} else {
ast.push(rule(name, objectToAst(value)))
}
}

return ast
}

export enum WalkAction {
/** Continue walking, which is the default */
Continue,
Expand All @@ -58,14 +78,17 @@ export function walk(
visit: (
node: AstNode,
utils: {
parent: AstNode | null
replaceWith(newNode: AstNode | AstNode[]): void
},
) => void | WalkAction,
parent: AstNode | null = null,
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
let status =
visit(node, {
parent,
replaceWith(newNode) {
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
// We want to visit the newly replaced node(s), which start at the
Expand All @@ -82,7 +105,7 @@ export function walk(
if (status === WalkAction.Skip) continue

if (node.kind === 'rule') {
walk(node.nodes, visit)
walk(node.nodes, visit, node)
}
}
}
Expand Down
32 changes: 29 additions & 3 deletions packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rule, type AstNode, type Rule } from './ast'
import { WalkAction, rule, walk, type AstNode, type Rule } from './ast'
import { type Candidate, type Variant } from './candidate'
import { type DesignSystem } from './design-system'
import GLOBAL_PROPERTY_ORDER from './property-order'
Expand Down Expand Up @@ -170,10 +170,25 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants):
let { applyFn } = variants.get(variant.root)!

if (variant.kind === 'compound') {
let result = applyVariant(node, variant.variant, variants)
// Some variants traverse the AST to mutate the nodes. E.g.: `group-*` wants
// to prefix every selector of the variant it's compounding with `.group`.
//
// E.g.:
// ```
// group-hover:[&_p]:flex
// ```
//
// Should only prefix the `group-hover` part with `.group`, and not the `&_p` part.
//
// To solve this, we provide an isolated placeholder node to the variant.
// The variant can now apply its logic to the isolated node without
// affecting the original node.
let isolatedNode = rule('@slot', [])

let result = applyVariant(isolatedNode, variant.variant, variants)
if (result === null) return null

for (let child of node.nodes) {
for (let child of isolatedNode.nodes) {
// Only some variants wrap children in rules. For example, the `force`
// variant is a noop on the AST. And the `has` variant modifies the
// selector rather than the children.
Expand All @@ -186,6 +201,17 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants):
let result = applyFn(child as Rule, variant)
if (result === null) return null
}

// Replace the placeholder node with the actual node
{
walk(isolatedNode.nodes, (child) => {
if (child.kind === 'rule' && child.nodes.length <= 0) {
child.nodes = node.nodes
return WalkAction.Skip
}
})
node.nodes = isolatedNode.nodes
}
return
}

Expand Down
18 changes: 2 additions & 16 deletions packages/tailwindcss/src/design-system.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rule, toCss } from './ast'
import { toCss } from './ast'
import { parseCandidate, parseVariant } from './candidate'
import { compileAstNodes, compileCandidates } from './compile'
import { getClassList, getVariants, type ClassEntry, type VariantEntry } from './intellisense'
Expand All @@ -8,10 +8,6 @@ import { Utilities, createUtilities } from './utilities'
import { DefaultMap } from './utils/default-map'
import { Variants, createVariants } from './variants'

export type Plugin = (api: {
addVariant: (name: string, selector: string | string[]) => void
}) => void

export type DesignSystem = {
theme: Theme
utilities: Utilities
Expand All @@ -29,7 +25,7 @@ export type DesignSystem = {
getUsedVariants(): ReturnType<typeof parseVariant>[]
}

export function buildDesignSystem(theme: Theme, plugins: Plugin[] = []): DesignSystem {
export function buildDesignSystem(theme: Theme): DesignSystem {
let utilities = createUtilities(theme)
let variants = createVariants(theme)

Expand Down Expand Up @@ -81,15 +77,5 @@ export function buildDesignSystem(theme: Theme, plugins: Plugin[] = []): DesignS
},
}

for (let plugin of plugins) {
plugin({
addVariant: (name: string, selectors: string | string[]) => {
variants.static(name, (r) => {
r.nodes = ([] as string[]).concat(selectors).map((selector) => rule(selector, r.nodes))
})
},
})
}

return designSystem
}
Loading
Loading