Skip to content

Commit 3dc3bad

Browse files
Re-introduce automatic var injection shorthand (#15020)
This PR re-introduces the automatic var injection feature. For some backstory, we used to support classes such as `bg-[--my-color]` that resolved as-if you wrote `bg-[var(--my-color)]`. The is issue is that some newer CSS properties accepts dashed-idents (without the `var(…)`). This means that some properties accept `view-timeline-name: --my-name;` (see: https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-name). To make this a tiny bit worse, these properties _also_ accept `var(--my-name-reference)` where the variable `--my-name-reference` could reference a dashed-ident such as `--my-name`. This makes the `bg-[--my-color]` ambiguous because we don't know if you want `var(--my-color)` or `--my-color`. With this PR, we bring back the automatic var injection feature as syntactic sugar, but we use a different syntax to avoid the ambiguity. Instead of `bg-[--my-color]`, you can now write `bg-(--my-color)` to get the same effect as `bg-[var(--my-color)]`. This also applies to modifiers, so `bg-red-500/[var(--my-opacity)]` can be written as `bg-red-500/(--my-opacity)`. To go full circle, you can rewrite `bg-[var(--my-color)]/[var(--my-opacity)]` as `bg-(--my-color)/(--my-opacity)`. --- This is implemented as syntactical sugar at the parsing stage and handled when re-printing. Internally the system (and every plugin) still see the proper `var(--my-color)` value. Since this is also handled during printing of the candidate, codemods don't need to be changed but they will provide the newly updated syntax. E.g.: running this on the Catalyst codebase, you'll now see changes like this: <img width="542" alt="image" src="https://github.com/user-attachments/assets/8f0e26f8-f4c9-4cdc-9f28-52307c38610e"> Whereas before we converted this to the much longer `min-w-[var(--button-width)]`. --- Additionally, this required some changes to the Oxide scanner to make sure that `(` and `)` are valid characters for arbitrary-like values. --------- Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
1 parent 9c3bfd6 commit 3dc3bad

File tree

11 files changed

+352
-76
lines changed

11 files changed

+352
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Reintroduce `max-w-screen-*` utilities that read from the `--breakpoint` namespace as deprecated utilities ([#15013](https://github.com/tailwindlabs/tailwindcss/pull/15013))
13+
- 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))
1314

1415
### Fixed
1516

crates/oxide/src/parser.rs

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ pub struct ExtractorOptions {
3939
pub preserve_spaces_in_arbitrary: bool,
4040
}
4141

42+
#[derive(Debug, PartialEq, Eq, Clone)]
43+
enum Arbitrary {
44+
/// Not inside any arbitrary value
45+
None,
46+
47+
/// In arbitrary value mode with square brackets
48+
///
49+
/// E.g.: `bg-[…]`
50+
/// ^
51+
Brackets { start_idx: usize },
52+
53+
/// In arbitrary value mode with parens
54+
///
55+
/// E.g.: `bg-(…)`
56+
/// ^
57+
Parens { start_idx: usize },
58+
}
59+
4260
pub struct Extractor<'a> {
4361
opts: ExtractorOptions,
4462

@@ -48,9 +66,9 @@ pub struct Extractor<'a> {
4866
idx_start: usize,
4967
idx_end: usize,
5068
idx_last: usize,
51-
idx_arbitrary_start: usize,
5269

53-
in_arbitrary: bool,
70+
arbitrary: Arbitrary,
71+
5472
in_candidate: bool,
5573
in_escape: bool,
5674

@@ -105,9 +123,8 @@ impl<'a> Extractor<'a> {
105123

106124
idx_start: 0,
107125
idx_end: 0,
108-
idx_arbitrary_start: 0,
109126

110-
in_arbitrary: false,
127+
arbitrary: Arbitrary::None,
111128
in_candidate: false,
112129
in_escape: false,
113130

@@ -461,7 +478,7 @@ impl<'a> Extractor<'a> {
461478

462479
#[inline(always)]
463480
fn parse_arbitrary(&mut self) -> ParseAction<'a> {
464-
// In this we could technically use memchr 6 times (then looped) to find the indexes / bounds of arbitrary valuesq
481+
// In this we could technically use memchr 6 times (then looped) to find the indexes / bounds of arbitrary values
465482
if self.in_escape {
466483
return self.parse_escaped();
467484
}
@@ -479,9 +496,29 @@ impl<'a> Extractor<'a> {
479496
self.bracket_stack.pop();
480497
}
481498

482-
// Last bracket is different compared to what we expect, therefore we are not in a
483-
// valid arbitrary value.
484-
_ if !self.in_quotes() => return ParseAction::Skip,
499+
// This is the last bracket meaning the end of arbitrary content
500+
_ if !self.in_quotes() => {
501+
if matches!(self.cursor.next, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') {
502+
return ParseAction::Consume;
503+
}
504+
505+
if let Arbitrary::Parens { start_idx } = self.arbitrary {
506+
trace!("Arbitrary::End\t");
507+
self.arbitrary = Arbitrary::None;
508+
509+
if self.cursor.pos - start_idx == 1 {
510+
// We have an empty arbitrary value, which is not allowed
511+
return ParseAction::Skip;
512+
}
513+
514+
// We have a valid arbitrary value
515+
return ParseAction::Consume;
516+
}
517+
518+
// Last parenthesis is different compared to what we expect, therefore we are
519+
// not in a valid arbitrary value.
520+
return ParseAction::Skip;
521+
}
485522

486523
// We're probably in quotes or nested brackets, so we keep going
487524
_ => {}
@@ -501,12 +538,14 @@ impl<'a> Extractor<'a> {
501538
return ParseAction::Consume;
502539
}
503540

504-
trace!("Arbitrary::End\t");
505-
self.in_arbitrary = false;
541+
if let Arbitrary::Brackets { start_idx } = self.arbitrary {
542+
trace!("Arbitrary::End\t");
543+
self.arbitrary = Arbitrary::None;
506544

507-
if self.cursor.pos - self.idx_arbitrary_start == 1 {
508-
// We have an empty arbitrary value, which is not allowed
509-
return ParseAction::Skip;
545+
if self.cursor.pos - start_idx == 1 {
546+
// We have an empty arbitrary value, which is not allowed
547+
return ParseAction::Skip;
548+
}
510549
}
511550
}
512551

@@ -531,9 +570,13 @@ impl<'a> Extractor<'a> {
531570
b' ' if !self.opts.preserve_spaces_in_arbitrary => {
532571
trace!("Arbitrary::SkipAndEndEarly\t");
533572

534-
// Restart the parser ahead of the arbitrary value
535-
// It may pick up more candidates
536-
return ParseAction::RestartAt(self.idx_arbitrary_start + 1);
573+
if let Arbitrary::Brackets { start_idx } | Arbitrary::Parens { start_idx } =
574+
self.arbitrary
575+
{
576+
// Restart the parser ahead of the arbitrary value It may pick up more
577+
// candidates
578+
return ParseAction::RestartAt(start_idx + 1);
579+
}
537580
}
538581

539582
// Arbitrary values allow any character inside them
@@ -550,11 +593,12 @@ impl<'a> Extractor<'a> {
550593
#[inline(always)]
551594
fn parse_start(&mut self) -> ParseAction<'a> {
552595
match self.cursor.curr {
553-
// Enter arbitrary value mode
596+
// Enter arbitrary property mode
554597
b'[' => {
555598
trace!("Arbitrary::Start\t");
556-
self.in_arbitrary = true;
557-
self.idx_arbitrary_start = self.cursor.pos;
599+
self.arbitrary = Arbitrary::Brackets {
600+
start_idx: self.cursor.pos,
601+
};
558602

559603
ParseAction::Consume
560604
}
@@ -584,22 +628,31 @@ impl<'a> Extractor<'a> {
584628
#[inline(always)]
585629
fn parse_continue(&mut self) -> ParseAction<'a> {
586630
match self.cursor.curr {
587-
// Enter arbitrary value mode
631+
// Enter arbitrary value mode. E.g.: `bg-[rgba(0, 0, 0)]`
632+
// ^
588633
b'[' if matches!(
589634
self.cursor.prev,
590635
b'@' | b'-' | b' ' | b':' | b'/' | b'!' | b'\0'
591636
) =>
592637
{
593638
trace!("Arbitrary::Start\t");
594-
self.in_arbitrary = true;
595-
self.idx_arbitrary_start = self.cursor.pos;
639+
self.arbitrary = Arbitrary::Brackets {
640+
start_idx: self.cursor.pos,
641+
};
596642
}
597643

598-
// Can't enter arbitrary value mode
599-
// This can't be a candidate
600-
b'[' => {
601-
trace!("Arbitrary::Skip_Start\t");
644+
// Enter arbitrary value mode. E.g.: `bg-(--my-color)`
645+
// ^
646+
b'(' if matches!(self.cursor.prev, b'-' | b'/') => {
647+
trace!("Arbitrary::Start\t");
648+
self.arbitrary = Arbitrary::Parens {
649+
start_idx: self.cursor.pos,
650+
};
651+
}
602652

653+
// Can't enter arbitrary value mode. This can't be a candidate.
654+
b'[' | b'(' => {
655+
trace!("Arbitrary::Skip_Start\t");
603656
return ParseAction::Skip;
604657
}
605658

@@ -684,7 +737,7 @@ impl<'a> Extractor<'a> {
684737
#[inline(always)]
685738
fn can_be_candidate(&mut self) -> bool {
686739
self.in_candidate
687-
&& !self.in_arbitrary
740+
&& matches!(self.arbitrary, Arbitrary::None)
688741
&& (0..=127).contains(&self.cursor.curr)
689742
&& (self.idx_start == 0 || self.input[self.idx_start - 1] <= 127)
690743
}
@@ -696,13 +749,13 @@ impl<'a> Extractor<'a> {
696749
self.idx_start = self.cursor.pos;
697750
self.idx_end = self.cursor.pos;
698751
self.in_candidate = false;
699-
self.in_arbitrary = false;
752+
self.arbitrary = Arbitrary::None;
700753
self.in_escape = false;
701754
}
702755

703756
#[inline(always)]
704757
fn parse_char(&mut self) -> ParseAction<'a> {
705-
if self.in_arbitrary {
758+
if !matches!(self.arbitrary, Arbitrary::None) {
706759
self.parse_arbitrary()
707760
} else if self.in_candidate {
708761
self.parse_continue()
@@ -732,9 +785,8 @@ impl<'a> Extractor<'a> {
732785

733786
self.idx_start = pos;
734787
self.idx_end = pos;
735-
self.idx_arbitrary_start = 0;
736788

737-
self.in_arbitrary = false;
789+
self.arbitrary = Arbitrary::None;
738790
self.in_candidate = false;
739791
self.in_escape = false;
740792

@@ -977,6 +1029,18 @@ mod test {
9771029
assert_eq!(candidates, vec!["m-[2px]"]);
9781030
}
9791031

1032+
#[test]
1033+
fn it_can_parse_utilities_with_arbitrary_var_shorthand() {
1034+
let candidates = run("m-(--my-var)", false);
1035+
assert_eq!(candidates, vec!["m-(--my-var)"]);
1036+
}
1037+
1038+
#[test]
1039+
fn it_can_parse_utilities_with_arbitrary_var_shorthand_as_modifier() {
1040+
let candidates = run("bg-(--my-color)/(--my-opacity)", false);
1041+
assert_eq!(candidates, vec!["bg-(--my-color)/(--my-opacity)"]);
1042+
}
1043+
9801044
#[test]
9811045
fn it_throws_away_arbitrary_values_that_are_unbalanced() {
9821046
let candidates = run("m-[calc(100px*2]", false);

integrations/upgrade/index.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ test(
100100
--- ./src/index.html ---
101101
<h1>🤠👋</h1>
102102
<div
103-
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)] max-w-[var(--breakpoint-md)] ml-[var(--breakpoint-md)]"
103+
class="flex! sm:block! bg-linear-to-t bg-(--my-red) max-w-(--breakpoint-md) ml-(--breakpoint-md)"
104104
></div>
105105
<!-- Migrate to sm -->
106106
<div class="blur-sm shadow-sm rounded-sm inset-shadow-sm drop-shadow-sm"></div>
@@ -151,9 +151,9 @@ test(
151151
candidate`flex!`,
152152
candidate`sm:block!`,
153153
candidate`bg-linear-to-t`,
154-
candidate`bg-[var(--my-red)]`,
155-
candidate`max-w-[var(--breakpoint-md)]`,
156-
candidate`ml-[var(--breakpoint-md)`,
154+
candidate`bg-(--my-red)`,
155+
candidate`max-w-(--breakpoint-md)`,
156+
candidate`ml-(--breakpoint-md)`,
157157
])
158158
},
159159
)
@@ -639,7 +639,7 @@ test(
639639
'src/index.html',
640640
// prettier-ignore
641641
js`
642-
<div class="bg-[var(--my-red)]"></div>
642+
<div class="bg-(--my-red)"></div>
643643
`,
644644
)
645645

@@ -798,7 +798,7 @@ test(
798798
'src/index.html',
799799
// prettier-ignore
800800
js`
801-
<div class="bg-[var(--my-red)]"></div>
801+
<div class="bg-(--my-red)"></div>
802802
`,
803803
)
804804

@@ -873,7 +873,7 @@ test(
873873
'src/index.html',
874874
// prettier-ignore
875875
js`
876-
<div class="bg-[var(--my-red)]"></div>
876+
<div class="bg-(--my-red)"></div>
877877
`,
878878
)
879879

@@ -1447,7 +1447,7 @@ test(
14471447
"
14481448
--- ./src/index.html ---
14491449
<div
1450-
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
1450+
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
14511451
></div>
14521452
14531453
--- ./src/root.1.css ---
@@ -1664,7 +1664,7 @@ test(
16641664
"
16651665
--- ./src/index.html ---
16661666
<div
1667-
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
1667+
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
16681668
></div>
16691669
16701670
--- ./src/index.css ---
@@ -1799,7 +1799,7 @@ test(
17991799
"
18001800
--- ./src/index.html ---
18011801
<div
1802-
class="flex! sm:block! bg-linear-to-t bg-[var(--my-red)]"
1802+
class="flex! sm:block! bg-linear-to-t bg-(--my-red)"
18031803
></div>
18041804
18051805
--- ./src/index.css ---

packages/@tailwindcss-upgrade/src/template/candidates.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const candidates = [
123123
['bg-[no-repeat_url(/image_13.png)]', 'bg-[no-repeat_url(/image_13.png)]'],
124124
[
125125
'bg-[var(--spacing-0_5,_var(--spacing-1_5,_3rem))]',
126-
'bg-[var(--spacing-0_5,var(--spacing-1_5,3rem))]',
126+
'bg-(--spacing-0_5,var(--spacing-1_5,3rem))',
127127
],
128128
]
129129

0 commit comments

Comments
 (0)