diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5e258e47118..85babe29c019 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Reintroduce `max-w-screen-*` utilities that read from the `--breakpoint` namespace as deprecated utilities ([#15013](https://github.com/tailwindlabs/tailwindcss/pull/15013))
+- Support using CSS variables as arbitrary values without `var(…)` by using parentheses instead of square brackets (e.g. `bg-(--my-color)`) ([#15020](https://github.com/tailwindlabs/tailwindcss/pull/15020))
### Fixed
diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs
index b58204d8712f..b32f36beaaa9 100644
--- a/crates/oxide/src/parser.rs
+++ b/crates/oxide/src/parser.rs
@@ -39,6 +39,24 @@ pub struct ExtractorOptions {
pub preserve_spaces_in_arbitrary: bool,
}
+#[derive(Debug, PartialEq, Eq, Clone)]
+enum Arbitrary {
+ /// Not inside any arbitrary value
+ None,
+
+ /// In arbitrary value mode with square brackets
+ ///
+ /// E.g.: `bg-[…]`
+ /// ^
+ Brackets { start_idx: usize },
+
+ /// In arbitrary value mode with parens
+ ///
+ /// E.g.: `bg-(…)`
+ /// ^
+ Parens { start_idx: usize },
+}
+
pub struct Extractor<'a> {
opts: ExtractorOptions,
@@ -48,9 +66,9 @@ pub struct Extractor<'a> {
idx_start: usize,
idx_end: usize,
idx_last: usize,
- idx_arbitrary_start: usize,
- in_arbitrary: bool,
+ arbitrary: Arbitrary,
+
in_candidate: bool,
in_escape: bool,
@@ -105,9 +123,8 @@ impl<'a> Extractor<'a> {
idx_start: 0,
idx_end: 0,
- idx_arbitrary_start: 0,
- in_arbitrary: false,
+ arbitrary: Arbitrary::None,
in_candidate: false,
in_escape: false,
@@ -461,7 +478,7 @@ impl<'a> Extractor<'a> {
#[inline(always)]
fn parse_arbitrary(&mut self) -> ParseAction<'a> {
- // In this we could technically use memchr 6 times (then looped) to find the indexes / bounds of arbitrary valuesq
+ // In this we could technically use memchr 6 times (then looped) to find the indexes / bounds of arbitrary values
if self.in_escape {
return self.parse_escaped();
}
@@ -479,9 +496,29 @@ impl<'a> Extractor<'a> {
self.bracket_stack.pop();
}
- // Last bracket is different compared to what we expect, therefore we are not in a
- // valid arbitrary value.
- _ if !self.in_quotes() => return ParseAction::Skip,
+ // This is the last bracket meaning the end of arbitrary content
+ _ if !self.in_quotes() => {
+ if matches!(self.cursor.next, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') {
+ return ParseAction::Consume;
+ }
+
+ if let Arbitrary::Parens { start_idx } = self.arbitrary {
+ trace!("Arbitrary::End\t");
+ self.arbitrary = Arbitrary::None;
+
+ if self.cursor.pos - start_idx == 1 {
+ // We have an empty arbitrary value, which is not allowed
+ return ParseAction::Skip;
+ }
+
+ // We have a valid arbitrary value
+ return ParseAction::Consume;
+ }
+
+ // Last parenthesis is different compared to what we expect, therefore we are
+ // not in a valid arbitrary value.
+ return ParseAction::Skip;
+ }
// We're probably in quotes or nested brackets, so we keep going
_ => {}
@@ -501,12 +538,14 @@ impl<'a> Extractor<'a> {
return ParseAction::Consume;
}
- trace!("Arbitrary::End\t");
- self.in_arbitrary = false;
+ if let Arbitrary::Brackets { start_idx } = self.arbitrary {
+ trace!("Arbitrary::End\t");
+ self.arbitrary = Arbitrary::None;
- if self.cursor.pos - self.idx_arbitrary_start == 1 {
- // We have an empty arbitrary value, which is not allowed
- return ParseAction::Skip;
+ if self.cursor.pos - start_idx == 1 {
+ // We have an empty arbitrary value, which is not allowed
+ return ParseAction::Skip;
+ }
}
}
@@ -531,9 +570,13 @@ impl<'a> Extractor<'a> {
b' ' if !self.opts.preserve_spaces_in_arbitrary => {
trace!("Arbitrary::SkipAndEndEarly\t");
- // Restart the parser ahead of the arbitrary value
- // It may pick up more candidates
- return ParseAction::RestartAt(self.idx_arbitrary_start + 1);
+ if let Arbitrary::Brackets { start_idx } | Arbitrary::Parens { start_idx } =
+ self.arbitrary
+ {
+ // Restart the parser ahead of the arbitrary value It may pick up more
+ // candidates
+ return ParseAction::RestartAt(start_idx + 1);
+ }
}
// Arbitrary values allow any character inside them
@@ -550,11 +593,12 @@ impl<'a> Extractor<'a> {
#[inline(always)]
fn parse_start(&mut self) -> ParseAction<'a> {
match self.cursor.curr {
- // Enter arbitrary value mode
+ // Enter arbitrary property mode
b'[' => {
trace!("Arbitrary::Start\t");
- self.in_arbitrary = true;
- self.idx_arbitrary_start = self.cursor.pos;
+ self.arbitrary = Arbitrary::Brackets {
+ start_idx: self.cursor.pos,
+ };
ParseAction::Consume
}
@@ -584,22 +628,31 @@ impl<'a> Extractor<'a> {
#[inline(always)]
fn parse_continue(&mut self) -> ParseAction<'a> {
match self.cursor.curr {
- // Enter arbitrary value mode
+ // Enter arbitrary value mode. E.g.: `bg-[rgba(0, 0, 0)]`
+ // ^
b'[' if matches!(
self.cursor.prev,
b'@' | b'-' | b' ' | b':' | b'/' | b'!' | b'\0'
) =>
{
trace!("Arbitrary::Start\t");
- self.in_arbitrary = true;
- self.idx_arbitrary_start = self.cursor.pos;
+ self.arbitrary = Arbitrary::Brackets {
+ start_idx: self.cursor.pos,
+ };
}
- // Can't enter arbitrary value mode
- // This can't be a candidate
- b'[' => {
- trace!("Arbitrary::Skip_Start\t");
+ // Enter arbitrary value mode. E.g.: `bg-(--my-color)`
+ // ^
+ b'(' if matches!(self.cursor.prev, b'-' | b'/') => {
+ trace!("Arbitrary::Start\t");
+ self.arbitrary = Arbitrary::Parens {
+ start_idx: self.cursor.pos,
+ };
+ }
+ // Can't enter arbitrary value mode. This can't be a candidate.
+ b'[' | b'(' => {
+ trace!("Arbitrary::Skip_Start\t");
return ParseAction::Skip;
}
@@ -684,7 +737,7 @@ impl<'a> Extractor<'a> {
#[inline(always)]
fn can_be_candidate(&mut self) -> bool {
self.in_candidate
- && !self.in_arbitrary
+ && matches!(self.arbitrary, Arbitrary::None)
&& (0..=127).contains(&self.cursor.curr)
&& (self.idx_start == 0 || self.input[self.idx_start - 1] <= 127)
}
@@ -696,13 +749,13 @@ impl<'a> Extractor<'a> {
self.idx_start = self.cursor.pos;
self.idx_end = self.cursor.pos;
self.in_candidate = false;
- self.in_arbitrary = false;
+ self.arbitrary = Arbitrary::None;
self.in_escape = false;
}
#[inline(always)]
fn parse_char(&mut self) -> ParseAction<'a> {
- if self.in_arbitrary {
+ if !matches!(self.arbitrary, Arbitrary::None) {
self.parse_arbitrary()
} else if self.in_candidate {
self.parse_continue()
@@ -732,9 +785,8 @@ impl<'a> Extractor<'a> {
self.idx_start = pos;
self.idx_end = pos;
- self.idx_arbitrary_start = 0;
- self.in_arbitrary = false;
+ self.arbitrary = Arbitrary::None;
self.in_candidate = false;
self.in_escape = false;
@@ -977,6 +1029,18 @@ mod test {
assert_eq!(candidates, vec!["m-[2px]"]);
}
+ #[test]
+ fn it_can_parse_utilities_with_arbitrary_var_shorthand() {
+ let candidates = run("m-(--my-var)", false);
+ assert_eq!(candidates, vec!["m-(--my-var)"]);
+ }
+
+ #[test]
+ fn it_can_parse_utilities_with_arbitrary_var_shorthand_as_modifier() {
+ let candidates = run("bg-(--my-color)/(--my-opacity)", false);
+ assert_eq!(candidates, vec!["bg-(--my-color)/(--my-opacity)"]);
+ }
+
#[test]
fn it_throws_away_arbitrary_values_that_are_unbalanced() {
let candidates = run("m-[calc(100px*2]", false);
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 5db4487b4803..ebc251a66c92 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -100,7 +100,7 @@ test(
--- ./src/index.html ---
🤠👋
@@ -151,9 +151,9 @@ test(
candidate`flex!`,
candidate`sm:block!`,
candidate`bg-linear-to-t`,
- candidate`bg-[var(--my-red)]`,
- candidate`max-w-[var(--breakpoint-md)]`,
- candidate`ml-[var(--breakpoint-md)`,
+ candidate`bg-(--my-red)`,
+ candidate`max-w-(--breakpoint-md)`,
+ candidate`ml-(--breakpoint-md)`,
])
},
)
@@ -639,7 +639,7 @@ test(
'src/index.html',
// prettier-ignore
js`
-
+
`,
)
@@ -798,7 +798,7 @@ test(
'src/index.html',
// prettier-ignore
js`
-
+
`,
)
@@ -873,7 +873,7 @@ test(
'src/index.html',
// prettier-ignore
js`
-
+
`,
)
@@ -1447,7 +1447,7 @@ test(
"
--- ./src/index.html ---
--- ./src/root.1.css ---
@@ -1664,7 +1664,7 @@ test(
"
--- ./src/index.html ---
--- ./src/index.css ---
@@ -1799,7 +1799,7 @@ test(
"
--- ./src/index.html ---
--- ./src/index.css ---
diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts
index 68c3783b7f5d..74f5974fcb8d 100644
--- a/packages/@tailwindcss-upgrade/src/template/candidates.test.ts
+++ b/packages/@tailwindcss-upgrade/src/template/candidates.test.ts
@@ -123,7 +123,7 @@ const candidates = [
['bg-[no-repeat_url(/image_13.png)]', 'bg-[no-repeat_url(/image_13.png)]'],
[
'bg-[var(--spacing-0_5,_var(--spacing-1_5,_3rem))]',
- 'bg-[var(--spacing-0_5,var(--spacing-1_5,3rem))]',
+ 'bg-(--spacing-0_5,var(--spacing-1_5,3rem))',
],
]
diff --git a/packages/@tailwindcss-upgrade/src/template/candidates.ts b/packages/@tailwindcss-upgrade/src/template/candidates.ts
index e4849a6be2a5..6a0ab65bd78f 100644
--- a/packages/@tailwindcss-upgrade/src/template/candidates.ts
+++ b/packages/@tailwindcss-upgrade/src/template/candidates.ts
@@ -42,12 +42,16 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate)
if (candidate.value) {
if (candidate.value.kind === 'arbitrary') {
- if (candidate.value === null) {
- base += ''
- } else if (candidate.value.dataType) {
- base += `-[${candidate.value.dataType}:${printArbitraryValue(candidate.value.value)}]`
- } else {
- base += `-[${printArbitraryValue(candidate.value.value)}]`
+ if (candidate.value !== null) {
+ let isVarValue = isVar(candidate.value.value)
+ let value = isVarValue ? candidate.value.value.slice(4, -1) : candidate.value.value
+ let [open, close] = isVarValue ? ['(', ')'] : ['[', ']']
+
+ if (candidate.value.dataType) {
+ base += `-${open}${candidate.value.dataType}:${printArbitraryValue(value)}${close}`
+ } else {
+ base += `-${open}${printArbitraryValue(value)}${close}`
+ }
}
} else if (candidate.value.kind === 'named') {
base += `-${candidate.value.value}`
@@ -63,8 +67,12 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate)
// Handle modifier
if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') {
if (candidate.modifier) {
+ let isVarValue = isVar(candidate.modifier.value)
+ let value = isVarValue ? candidate.modifier.value.slice(4, -1) : candidate.modifier.value
+ let [open, close] = isVarValue ? ['(', ')'] : ['[', ']']
+
if (candidate.modifier.kind === 'arbitrary') {
- base += `/[${printArbitraryValue(candidate.modifier.value)}]`
+ base += `/${open}${printArbitraryValue(value)}${close}`
} else if (candidate.modifier.kind === 'named') {
base += `/${candidate.modifier.value}`
}
@@ -99,7 +107,11 @@ function printVariant(variant: Variant) {
base += variant.root
if (variant.value) {
if (variant.value.kind === 'arbitrary') {
- base += `-[${printArbitraryValue(variant.value.value)}]`
+ let isVarValue = isVar(variant.value.value)
+ let value = isVarValue ? variant.value.value.slice(4, -1) : variant.value.value
+ let [open, close] = isVarValue ? ['(', ')'] : ['[', ']']
+
+ base += `-${open}${printArbitraryValue(value)}${close}`
} else if (variant.value.kind === 'named') {
base += `-${variant.value.value}`
}
@@ -246,9 +258,15 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) {
break
}
case 'separator':
- case 'word': {
node.value = escapeUnderscore(node.value)
break
+ case 'word': {
+ // Dashed idents and variables `var(--my-var)` and `--my-var` should not
+ // have underscores escaped
+ if (node.value[0] !== '-' && node.value[1] !== '-') {
+ node.value = escapeUnderscore(node.value)
+ }
+ break
}
default:
never(node)
@@ -256,6 +274,11 @@ function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) {
}
}
+function isVar(value: string) {
+ let ast = ValueParser.parse(value)
+ return ast.length === 1 && ast[0].kind === 'function' && ast[0].value === 'var'
+}
+
function never(value: never): never {
throw new Error(`Unexpected value: ${value}`)
}
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts
index b3ddb06d338a..d9e714e9e1e5 100644
--- a/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/automatic-var-injection.test.ts
@@ -9,10 +9,10 @@ test.each([
['[--my-color:--my-other-color]', '[--my-color:var(--my-other-color)]'],
// Arbitrary values for functional candidates
- ['bg-[--my-color]', 'bg-[var(--my-color)]'],
- ['bg-[color:--my-color]', 'bg-[color:var(--my-color)]'],
- ['border-[length:--my-length]', 'border-[length:var(--my-length)]'],
- ['border-[line-width:--my-width]', 'border-[line-width:var(--my-width)]'],
+ ['bg-[--my-color]', 'bg-(--my-color)'],
+ ['bg-[color:--my-color]', 'bg-(color:--my-color)'],
+ ['border-[length:--my-length]', 'border-(length:--my-length)'],
+ ['border-[line-width:--my-width]', 'border-(line-width:--my-width)'],
// Can clean up the workaround for opting out of automatic var injection
['bg-[_--my-color]', 'bg-[--my-color]'],
@@ -21,19 +21,19 @@ test.each([
['border-[line-width:_--my-width]', 'border-[line-width:--my-width]'],
// Modifiers
- ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/[var(--my-opacity)]'],
- ['bg-red-500/[--my-opacity]', 'bg-red-500/[var(--my-opacity)]'],
- ['bg-[--my-color]/[--my-opacity]', 'bg-[var(--my-color)]/[var(--my-opacity)]'],
- ['bg-[color:--my-color]/[--my-opacity]', 'bg-[color:var(--my-color)]/[var(--my-opacity)]'],
+ ['[color:--my-color]/[--my-opacity]', '[color:var(--my-color)]/(--my-opacity)'],
+ ['bg-red-500/[--my-opacity]', 'bg-red-500/(--my-opacity)'],
+ ['bg-[--my-color]/[--my-opacity]', 'bg-(--my-color)/(--my-opacity)'],
+ ['bg-[color:--my-color]/[--my-opacity]', 'bg-(color:--my-color)/(--my-opacity)'],
// Can clean up the workaround for opting out of automatic var injection
['[color:--my-color]/[_--my-opacity]', '[color:var(--my-color)]/[--my-opacity]'],
['bg-red-500/[_--my-opacity]', 'bg-red-500/[--my-opacity]'],
- ['bg-[--my-color]/[_--my-opacity]', 'bg-[var(--my-color)]/[--my-opacity]'],
- ['bg-[color:--my-color]/[_--my-opacity]', 'bg-[color:var(--my-color)]/[--my-opacity]'],
+ ['bg-[--my-color]/[_--my-opacity]', 'bg-(--my-color)/[--my-opacity]'],
+ ['bg-[color:--my-color]/[_--my-opacity]', 'bg-(color:--my-color)/[--my-opacity]'],
// Variants
- ['supports-[--test]:flex', 'supports-[var(--test)]:flex'],
+ ['supports-[--test]:flex', 'supports-(--test):flex'],
['supports-[_--test]:flex', 'supports-[--test]:flex'],
// Some properties never had var() injection in v3.
diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts
index 2bad8ba80912..f5f2a41db31a 100644
--- a/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts
+++ b/packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts
@@ -19,11 +19,11 @@ test.each([
// Convert to `var(…)` if we can resolve the path
['[color:theme(colors.red.500)]', '[color:var(--color-red-500)]'], // Arbitrary property
['[color:theme(colors.red.500)]/50', '[color:var(--color-red-500)]/50'], // Arbitrary property + modifier
- ['bg-[theme(colors.red.500)]', 'bg-[var(--color-red-500)]'], // Arbitrary value
+ ['bg-[theme(colors.red.500)]', 'bg-(--color-red-500)'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-[size:calc(var(--spacing)*4)]'], // Arbitrary value + data type hint
// Convert to `var(…)` if we can resolve the path, but keep fallback values
- ['bg-[theme(colors.red.500,red)]', 'bg-[var(--color-red-500,red)]'],
+ ['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'],
// Keep `theme(…)` if we can't resolve the path
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],
@@ -66,13 +66,13 @@ test.each([
// Arbitrary property, with more complex modifier (we only allow whole numbers
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
['[color:theme(colors.red.500/12.34%)]', '[color:var(--color-red-500)]/[12.34%]'],
- ['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/[var(--opacity)]'],
+ ['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/(--opacity)'],
['[color:theme(colors.red.500/.12345)]', '[color:var(--color-red-500)]/[12.345]'],
['[color:theme(colors.red.500/50.25%)]', '[color:var(--color-red-500)]/[50.25%]'],
// Arbitrary value
- ['bg-[theme(colors.red.500/75%)]', 'bg-[var(--color-red-500)]/75'],
- ['bg-[theme(colors.red.500/12.34%)]', 'bg-[var(--color-red-500)]/[12.34%]'],
+ ['bg-[theme(colors.red.500/75%)]', 'bg-(--color-red-500)/75'],
+ ['bg-[theme(colors.red.500/12.34%)]', 'bg-(--color-red-500)/[12.34%]'],
// Arbitrary property that already contains a modifier
['[color:theme(colors.red.500/50%)]/50', '[color:theme(--color-red-500/50%)]/50'],
@@ -109,8 +109,8 @@ test.each([
['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'],
// Renamed theme keys
- ['max-w-[theme(screens.md)]', 'max-w-[var(--breakpoint-md)]'],
- ['w-[theme(maxWidth.md)]', 'w-[var(--container-md)]'],
+ ['max-w-[theme(screens.md)]', 'max-w-(--breakpoint-md)'],
+ ['w-[theme(maxWidth.md)]', 'w-(--container-md)'],
// Invalid cases
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
diff --git a/packages/tailwindcss/src/candidate.test.ts b/packages/tailwindcss/src/candidate.test.ts
index 570a3424977a..d7f458061715 100644
--- a/packages/tailwindcss/src/candidate.test.ts
+++ b/packages/tailwindcss/src/candidate.test.ts
@@ -167,6 +167,35 @@ it('should parse a simple utility with an arbitrary variant', () => {
`)
})
+it('should parse an arbitrary variant using the automatic var shorthand', () => {
+ let utilities = new Utilities()
+ utilities.static('flex', () => [])
+ let variants = new Variants()
+ variants.functional('supports', () => {})
+
+ expect(run('supports-(--test):flex', { utilities, variants })).toMatchInlineSnapshot(`
+ [
+ {
+ "important": false,
+ "kind": "static",
+ "raw": "supports-(--test):flex",
+ "root": "flex",
+ "variants": [
+ {
+ "kind": "functional",
+ "modifier": null,
+ "root": "supports",
+ "value": {
+ "kind": "arbitrary",
+ "value": "var(--test)",
+ },
+ },
+ ],
+ },
+ ]
+ `)
+})
+
it('should parse a simple utility with a parameterized variant', () => {
let utilities = new Utilities()
utilities.static('flex', () => [])
@@ -511,6 +540,29 @@ it('should parse a utility with an arbitrary value', () => {
`)
})
+it('should parse a utility with an arbitrary value with parens', () => {
+ let utilities = new Utilities()
+ utilities.functional('bg', () => [])
+
+ expect(run('bg-(--my-color)', { utilities })).toMatchInlineSnapshot(`
+ [
+ {
+ "important": false,
+ "kind": "functional",
+ "modifier": null,
+ "raw": "bg-(--my-color)",
+ "root": "bg",
+ "value": {
+ "dataType": null,
+ "kind": "arbitrary",
+ "value": "var(--my-color)",
+ },
+ "variants": [],
+ },
+ ]
+ `)
+})
+
it('should parse a utility with an arbitrary value including a typehint', () => {
let utilities = new Utilities()
utilities.functional('bg', () => [])
@@ -534,6 +586,52 @@ it('should parse a utility with an arbitrary value including a typehint', () =>
`)
})
+it('should parse a utility with an arbitrary value with parens including a typehint', () => {
+ let utilities = new Utilities()
+ utilities.functional('bg', () => [])
+
+ expect(run('bg-(color:--my-color)', { utilities })).toMatchInlineSnapshot(`
+ [
+ {
+ "important": false,
+ "kind": "functional",
+ "modifier": null,
+ "raw": "bg-(color:--my-color)",
+ "root": "bg",
+ "value": {
+ "dataType": "color",
+ "kind": "arbitrary",
+ "value": "var(--my-color)",
+ },
+ "variants": [],
+ },
+ ]
+ `)
+})
+
+it('should parse a utility with an arbitrary value with parens and a fallback', () => {
+ let utilities = new Utilities()
+ utilities.functional('bg', () => [])
+
+ expect(run('bg-(color:--my-color,#0088cc)', { utilities })).toMatchInlineSnapshot(`
+ [
+ {
+ "important": false,
+ "kind": "functional",
+ "modifier": null,
+ "raw": "bg-(color:--my-color,#0088cc)",
+ "root": "bg",
+ "value": {
+ "dataType": "color",
+ "kind": "arbitrary",
+ "value": "var(--my-color,#0088cc)",
+ },
+ "variants": [],
+ },
+ ]
+ `)
+})
+
it('should parse a utility with an arbitrary value with a modifier', () => {
let utilities = new Utilities()
utilities.functional('bg', () => [])
@@ -757,6 +855,32 @@ it('should parse a utility with an implicit variable as the modifier', () => {
`)
})
+it('should parse a utility with an implicit variable as the modifier using the shorthand', () => {
+ let utilities = new Utilities()
+ utilities.functional('bg', () => [])
+
+ expect(run('bg-red-500/(--value)', { utilities })).toMatchInlineSnapshot(`
+ [
+ {
+ "important": false,
+ "kind": "functional",
+ "modifier": {
+ "kind": "arbitrary",
+ "value": "var(--value)",
+ },
+ "raw": "bg-red-500/(--value)",
+ "root": "bg",
+ "value": {
+ "fraction": null,
+ "kind": "named",
+ "value": "red-500",
+ },
+ "variants": [],
+ },
+ ]
+ `)
+})
+
it('should parse a utility with an implicit variable as the modifier that is important', () => {
let utilities = new Utilities()
utilities.functional('bg', () => [])
diff --git a/packages/tailwindcss/src/candidate.ts b/packages/tailwindcss/src/candidate.ts
index 669ef4f7c103..c5c8bd85c679 100644
--- a/packages/tailwindcss/src/candidate.ts
+++ b/packages/tailwindcss/src/candidate.ts
@@ -14,6 +14,9 @@ type ArbitraryUtilityValue = {
* ```
* bg-[color:var(--my-color)]
* ^^^^^
+ *
+ * bg-(color:--my-color)
+ * ^^^^^
* ```
*/
dataType: string | null
@@ -25,6 +28,9 @@ type ArbitraryUtilityValue = {
*
* bg-[var(--my_variable)]
* ^^^^^^^^^^^^^^^^^^
+ *
+ * bg-(--my_variable)
+ * ^^^^^^^^^^^^^^
* ```
*/
value: string
@@ -340,9 +346,9 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
// ^^ -> Root
// ^^^^^^^^^ -> Arbitrary value
//
- // bg-red-[#0088cc]
- // ^^^^^^ -> Root
- // ^^^^^^^^^ -> Arbitrary value
+ // border-l-[#0088cc]
+ // ^^^^^^^^ -> Root
+ // ^^^^^^^^^ -> Arbitrary value
// ```
if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') {
let idx = baseWithoutModifier.indexOf('-[')
@@ -359,6 +365,43 @@ export function* parseCandidate(input: string, designSystem: DesignSystem): Iter
roots = [[root, value]]
}
+ // If the base of the utility ends with a `)`, then we know it's an arbitrary
+ // value that encapsulates a CSS variable. This also means that everything
+ // before the `(…)` part should be the root of the utility.
+ //
+ // E.g.:
+ //
+ // bg-(--my-var)
+ // ^^ -> Root
+ // ^^^^^^^^^^ -> Arbitrary value
+ // ```
+ else if (baseWithoutModifier[baseWithoutModifier.length - 1] === ')') {
+ let idx = baseWithoutModifier.indexOf('-(')
+ if (idx === -1) return
+
+ let root = baseWithoutModifier.slice(0, idx)
+
+ // The root of the utility should exist as-is in the utilities map. If not,
+ // it's an invalid utility and we can skip continue parsing.
+ if (!designSystem.utilities.has(root, 'functional')) return
+
+ let value = baseWithoutModifier.slice(idx + 2, -1)
+
+ let parts = segment(value, ':')
+
+ let dataType = null
+ if (parts.length === 2) {
+ dataType = parts[0]
+ value = parts[1]
+ }
+
+ // An arbitrary value with `(…)` should always start with `--` since it
+ // represents a CSS variable.
+ if (value[0] !== '-' && value[1] !== '-') return
+
+ roots = [[root, dataType === null ? `[var(${value})]` : `[${dataType}:var(${value})]`]]
+ }
+
// Not an arbitrary value
else {
roots = findRoots(baseWithoutModifier, (root: string) => {
@@ -446,6 +489,15 @@ function parseModifier(modifier: string): CandidateModifier {
}
}
+ if (modifier[0] === '(' && modifier[modifier.length - 1] === ')') {
+ let arbitraryValue = modifier.slice(1, -1)
+
+ return {
+ kind: 'arbitrary',
+ value: decodeArbitraryValue(`var(${arbitraryValue})`),
+ }
+ }
+
return {
kind: 'named',
value: modifier,
@@ -546,6 +598,18 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
}
}
+ if (value[0] === '(' && value[value.length - 1] === ')') {
+ return {
+ kind: 'functional',
+ root,
+ modifier: modifier === null ? null : parseModifier(modifier),
+ value: {
+ kind: 'arbitrary',
+ value: decodeArbitraryValue(`var(${value.slice(1, -1)})`),
+ },
+ }
+ }
+
return {
kind: 'functional',
root,
diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts
index 4e44129cb96c..ece3d78f8324 100644
--- a/packages/tailwindcss/src/index.test.ts
+++ b/packages/tailwindcss/src/index.test.ts
@@ -127,7 +127,7 @@ describe('compiling CSS', () => {
@tailwind utilities;
`,
[
- 'bg-[no-repeat_url(./my_file.jpg)',
+ 'bg-[no-repeat_url(./my_file.jpg)]',
'ml-[var(--spacing-1_5,_var(--spacing-2_5,_1rem))]',
'ml-[theme(--spacing-1_5,theme(--spacing-2_5,_1rem)))]',
],
@@ -146,8 +146,8 @@ describe('compiling CSS', () => {
margin-left: var(--spacing-1_5, var(--spacing-2_5, 1rem));
}
- .bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\) {
- background-color: no-repeat url("./")my file. jpg;
+ .bg-\\[no-repeat_url\\(\\.\\/my_file\\.jpg\\)\\] {
+ background-color: no-repeat url("./my_file.jpg");
}"
`)
})
diff --git a/packages/tailwindcss/src/intellisense.test.ts b/packages/tailwindcss/src/intellisense.test.ts
index cccf3d4eb14a..e13ce4ab1b3a 100644
--- a/packages/tailwindcss/src/intellisense.test.ts
+++ b/packages/tailwindcss/src/intellisense.test.ts
@@ -365,7 +365,7 @@ test('Functional utilities from plugins are listed in hovers and completions', a
expect(classNames).not.toContain('custom-2-unknown')
- // matchUtilities with a any modifiers
+ // matchUtilities with any modifiers
expect(classNames).toContain('custom-3-red')
expect(classMap.get('custom-3-red')?.modifiers).toEqual([])