Skip to content

Commit af7e610

Browse files
authored
Improve relative precedence of rtl, ltr, forced-colors and dark variants (#12584)
* Reduce specificity of `rtl`, `ltr`, and `dark` variants Reduce specificity of `rtl`, `ltr`, and `dark` variants (when using `darkMode: 'class'`) to make them the same as other variants. This also sorts the LTR/RTL and dark variants later in the variant plugin list to ensure that the reduced specificity doesn't cause them to start "losing" to other variants to keep things as backwards compatible as possible. Resolves a long-standing issue where `darkMode: 'media'` and `darkMode: 'class'` had different specificity, which meant switching your dark mode strategy could break your site. * Update changelog --------- Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
1 parent c5d3aab commit af7e610

16 files changed

+136
-112
lines changed

CHANGELOG.md

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

4040
- Simplify the `sans` font-family stack ([#11748](https://github.com/tailwindlabs/tailwindcss/pull/11748))
4141
- Disable the tap highlight overlay on iOS ([#12299](https://github.com/tailwindlabs/tailwindcss/pull/12299))
42+
- Improve relative precedence of `rtl`, `ltr`, `forced-colors`, and `dark` variants ([#12584](https://github.com/tailwindlabs/tailwindcss/pull/12584))
4243
- [Oxide] Deprecate `--no-autoprefixer` flag in the CLI ([#11280](https://github.com/tailwindlabs/tailwindcss/pull/11280))
4344
- [Oxide] Make the Rust based parser the default ([#11394](https://github.com/tailwindlabs/tailwindcss/pull/11394))
4445

src/corePlugins.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ export let variantPlugins = {
206206
},
207207

208208
directionVariants: ({ addVariant }) => {
209-
addVariant('ltr', ':is([dir="ltr"] &)')
210-
addVariant('rtl', ':is([dir="rtl"] &)')
209+
addVariant('ltr', ':is(:where([dir="ltr"]) &)')
210+
addVariant('rtl', ':is(:where([dir="rtl"]) &)')
211211
},
212212

213213
reducedMotionVariants: ({ addVariant }) => {
@@ -228,7 +228,7 @@ export let variantPlugins = {
228228
}
229229

230230
if (mode === 'class') {
231-
addVariant('dark', `:is(${className} &)`)
231+
addVariant('dark', `:is(:where(${className}) &)`)
232232
} else if (mode === 'media') {
233233
addVariant('dark', '@media (prefers-color-scheme: dark)')
234234
}

src/lib/setupContextUtils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -755,14 +755,14 @@ function resolvePlugins(context, root) {
755755
]
756756
let afterVariants = [
757757
variantPlugins['supportsVariants'],
758-
variantPlugins['directionVariants'],
759758
variantPlugins['reducedMotionVariants'],
760759
variantPlugins['prefersContrastVariants'],
761-
variantPlugins['darkVariants'],
762-
variantPlugins['forcedColorsVariants'],
763760
variantPlugins['printVariant'],
764761
variantPlugins['screenVariants'],
765762
variantPlugins['orientationVariants'],
763+
variantPlugins['directionVariants'],
764+
variantPlugins['darkVariants'],
765+
variantPlugins['forcedColorsVariants'],
766766
]
767767

768768
return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { applyImportantSelector } from '../src/util/applyImportantSelector'
22

33
it.each`
4-
before | after
5-
${'.foo'} | ${'#app :is(.foo)'}
6-
${'.foo .bar'} | ${'#app :is(.foo .bar)'}
7-
${'.foo:hover'} | ${'#app :is(.foo:hover)'}
8-
${'.foo .bar:hover'} | ${'#app :is(.foo .bar:hover)'}
9-
${'.foo::before'} | ${'#app :is(.foo)::before'}
10-
${'.foo::before'} | ${'#app :is(.foo)::before'}
11-
${'.foo::file-selector-button'} | ${'#app :is(.foo)::file-selector-button'}
12-
${'.foo::-webkit-progress-bar'} | ${'#app :is(.foo)::-webkit-progress-bar'}
13-
${'.foo:hover::before'} | ${'#app :is(.foo:hover)::before'}
14-
${':is(.dark :is([dir="rtl"] .foo::before))'} | ${'#app :is(.dark :is([dir="rtl"] .foo))::before'}
15-
${':is(.dark .foo) .bar'} | ${'#app :is(:is(.dark .foo) .bar)'}
16-
${':is(.foo) :is(.bar)'} | ${'#app :is(:is(.foo) :is(.bar))'}
17-
${':is(.foo)::before'} | ${'#app :is(.foo)::before'}
18-
${'.foo:before'} | ${'#app :is(.foo):before'}
19-
${'.foo::some-unknown-pseudo'} | ${'#app :is(.foo)::some-unknown-pseudo'}
20-
${'.foo::some-unknown-pseudo:hover'} | ${'#app :is(.foo)::some-unknown-pseudo:hover'}
21-
${'.foo:focus::some-unknown-pseudo:hover'} | ${'#app :is(.foo:focus)::some-unknown-pseudo:hover'}
22-
${'.foo:hover::some-unknown-pseudo:focus'} | ${'#app :is(.foo:hover)::some-unknown-pseudo:focus'}
4+
before | after
5+
${'.foo'} | ${'#app :is(.foo)'}
6+
${'.foo .bar'} | ${'#app :is(.foo .bar)'}
7+
${'.foo:hover'} | ${'#app :is(.foo:hover)'}
8+
${'.foo .bar:hover'} | ${'#app :is(.foo .bar:hover)'}
9+
${'.foo::before'} | ${'#app :is(.foo)::before'}
10+
${'.foo::before'} | ${'#app :is(.foo)::before'}
11+
${'.foo::file-selector-button'} | ${'#app :is(.foo)::file-selector-button'}
12+
${'.foo::-webkit-progress-bar'} | ${'#app :is(.foo)::-webkit-progress-bar'}
13+
${'.foo:hover::before'} | ${'#app :is(.foo:hover)::before'}
14+
${':is(:where(.dark) :is(:where([dir="rtl"]) .foo::before))'} | ${'#app :is(:where(.dark) :is(:where([dir="rtl"]) .foo))::before'}
15+
${':is(:where(.dark) .foo) .bar'} | ${'#app :is(:is(:where(.dark) .foo) .bar)'}
16+
${':is(.foo) :is(.bar)'} | ${'#app :is(:is(.foo) :is(.bar))'}
17+
${':is(.foo)::before'} | ${'#app :is(.foo)::before'}
18+
${'.foo:before'} | ${'#app :is(.foo):before'}
19+
${'.foo::some-unknown-pseudo'} | ${'#app :is(.foo)::some-unknown-pseudo'}
20+
${'.foo::some-unknown-pseudo:hover'} | ${'#app :is(.foo)::some-unknown-pseudo:hover'}
21+
${'.foo:focus::some-unknown-pseudo:hover'} | ${'#app :is(.foo:focus)::some-unknown-pseudo:hover'}
22+
${'.foo:hover::some-unknown-pseudo:focus'} | ${'#app :is(.foo:hover)::some-unknown-pseudo:focus'}
2323
`('should generate "$after" from "$before"', ({ before, after }) => {
2424
expect(applyImportantSelector(before, '#app')).toEqual(after)
2525
})

tests/apply.test.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,14 @@ test('@apply', () => {
215215
text-align: left;
216216
}
217217
}
218-
:is(.dark .apply-dark-variant) {
218+
:is(:where(.dark) .apply-dark-variant) {
219219
text-align: center;
220220
}
221-
:is(.dark .apply-dark-variant:hover) {
221+
:is(:where(.dark) .apply-dark-variant:hover) {
222222
text-align: right;
223223
}
224224
@media (min-width: 1024px) {
225-
:is(.dark .apply-dark-variant) {
225+
:is(:where(.dark) .apply-dark-variant) {
226226
text-align: left;
227227
}
228228
}
@@ -2016,24 +2016,28 @@ it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
20162016

20172017
return run(input, config).then((result) => {
20182018
expect(result.css).toMatchFormattedCss(css`
2019-
:is(.dark .foo):before,
2020-
:is([dir='rtl'] :is(.dark .bar)):before,
2021-
:is([dir='rtl'] :is(.dark .baz:hover)):before {
2019+
:is(:where(.dark) .foo):before,
2020+
:is(:where([dir='rtl']) :is(:where(.dark) .bar)):before,
2021+
:is(:where([dir='rtl']) :is(:where(.dark) .baz:hover)):before {
20222022
background-color: #000;
20232023
}
2024-
:-webkit-any([dir='rtl'] :-webkit-any(.dark .qux))::-webkit-file-upload-button:hover {
2024+
:-webkit-any(
2025+
:where([dir='rtl']) :-webkit-any(:where(.dark) .qux)
2026+
)::-webkit-file-upload-button:hover {
20252027
background-color: #000;
20262028
}
2027-
:is([dir='rtl'] :is(.dark .qux))::file-selector-button:hover {
2029+
:is(:where([dir='rtl']) :is(:where(.dark) .qux))::file-selector-button:hover {
20282030
background-color: #000;
20292031
}
2030-
:is([dir='rtl'] :is(.dark .steve):hover):before {
2032+
:is(:where([dir='rtl']) :is(:where(.dark) .steve):hover):before {
20312033
background-color: #000;
20322034
}
2033-
:-webkit-any([dir='rtl'] :-webkit-any(.dark .bob))::-webkit-file-upload-button:hover {
2035+
:-webkit-any(
2036+
:where([dir='rtl']) :-webkit-any(:where(.dark) .bob)
2037+
)::-webkit-file-upload-button:hover {
20342038
background-color: #000;
20352039
}
2036-
:is([dir='rtl'] :is(.dark .bob))::file-selector-button:hover {
2040+
:is(:where([dir='rtl']) :is(:where(.dark) .bob))::file-selector-button:hover {
20372041
background-color: #000;
20382042
}
20392043
:has([dir='rtl'] .foo:hover):before {

tests/custom-separator.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@ test('custom separator', () => {
2222
.group:hover .group-hover_focus-within_text-left:focus-within {
2323
text-align: left;
2424
}
25-
:is([dir='rtl'] .rtl_active_text-center:active) {
26-
text-align: center;
27-
}
2825
@media (prefers-reduced-motion: no-preference) {
2926
.motion-safe_hover_text-center:hover {
3027
text-align: center;
3128
}
3229
}
33-
:is(.dark .dark_focus_text-left:focus) {
34-
text-align: left;
35-
}
3630
@media (min-width: 768px) {
3731
.md_hover_text-right:hover {
3832
text-align: right;
3933
}
4034
}
35+
:is(:where([dir='rtl']) .rtl_active_text-center:active) {
36+
text-align: center;
37+
}
38+
:is(:where(.dark) .dark_focus_text-left:focus) {
39+
text-align: left;
40+
}
4141
`)
4242
})
4343
})

tests/dark-mode.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ it('should be possible to use the darkMode "class" mode', () => {
1616
return run(input, config).then((result) => {
1717
expect(result.css).toMatchFormattedCss(css`
1818
${defaults}
19-
:is(.dark .dark\:font-bold) {
19+
:is(:where(.dark) .dark\:font-bold) {
2020
font-weight: 700;
2121
}
2222
`)
@@ -39,7 +39,7 @@ it('should be possible to change the class name', () => {
3939
return run(input, config).then((result) => {
4040
expect(result.css).toMatchFormattedCss(css`
4141
${defaults}
42-
:is(.test-dark .dark\:font-bold) {
42+
:is(:where(.test-dark) .dark\:font-bold) {
4343
font-weight: 700;
4444
}
4545
`)

tests/format-variant-selector.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ describe('pseudo elements', () => {
343343
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
344344
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
345345
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
346-
${'#app :is(.dark &::before)'} | ${'#app :is(.dark &)::before'}
347-
${'#app :is(:is(.dark &)::before)'} | ${'#app :is(:is(.dark &))::before'}
346+
${'#app :is(:where(.dark) &::before)'} | ${'#app :is(:where(.dark) &)::before'}
347+
${'#app :is(:is(:where(.dark) &)::before)'} | ${'#app :is(:is(:where(.dark) &))::before'}
348348
${'#app :is(.foo::file-selector-button)'} | ${'#app :is(.foo)::file-selector-button'}
349349
${'#app :is(.foo::-webkit-progress-bar)'} | ${'#app :is(.foo)::-webkit-progress-bar'}
350350
${'.parent::marker li'} | ${'.parent li::marker'}

tests/important-boolean.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,22 @@ test('important boolean', () => {
137137
.group:hover .group-hover\:focus-within\:text-left:focus-within {
138138
text-align: left !important;
139139
}
140-
:is([dir='rtl'] .rtl\:active\:text-center:active) {
141-
text-align: center !important;
142-
}
143140
@media (prefers-reduced-motion: no-preference) {
144141
.motion-safe\:hover\:text-center:hover {
145142
text-align: center !important;
146143
}
147144
}
148-
:is(.dark .dark\:focus\:text-left:focus) {
149-
text-align: left !important;
150-
}
151145
@media (min-width: 768px) {
152146
.md\:hover\:text-right:hover {
153147
text-align: right !important;
154148
}
155149
}
150+
:is(:where([dir='rtl']) .rtl\:active\:text-center:active) {
151+
text-align: center !important;
152+
}
153+
:is(:where(.dark) .dark\:focus\:text-left:focus) {
154+
text-align: left !important;
155+
}
156156
`)
157157
})
158158
})

tests/important-selector.test.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,36 +135,39 @@ test('important selector', () => {
135135
#app :is(.group:hover .group-hover\:focus-within\:text-left:focus-within) {
136136
text-align: left;
137137
}
138-
#app :is([dir='rtl'] .rtl\:active\:text-center:active) {
139-
text-align: center;
140-
}
141138
@media (prefers-reduced-motion: no-preference) {
142139
#app .motion-safe\:hover\:text-center:hover {
143140
text-align: center;
144141
}
145142
}
146-
#app :is(.dark .dark\:before\:underline):before {
147-
content: var(--tw-content);
148-
text-decoration-line: underline;
149-
}
150-
#app :is(.dark .dark\:focus\:text-left:focus) {
151-
text-align: left;
152-
}
153143
@media (min-width: 768px) {
154144
#app .md\:hover\:text-right:hover {
155145
text-align: right;
156146
}
157147
}
148+
#app :is(:where([dir='rtl']) .rtl\:active\:text-center:active) {
149+
text-align: center;
150+
}
151+
#app :is(:where(.dark) .dark\:before\:underline):before {
152+
content: var(--tw-content);
153+
text-decoration-line: underline;
154+
}
155+
#app :is(:where(.dark) .dark\:focus\:text-left:focus) {
156+
text-align: left;
157+
}
158158
#app
159159
:-webkit-any(
160-
[dir='rtl']
161-
:-webkit-any(.dark .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
160+
:where([dir='rtl'])
161+
:-webkit-any(
162+
:where(.dark) .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100
163+
)
162164
)::-webkit-file-upload-button:hover {
163165
background-color: #000;
164166
}
165167
#app
166168
:is(
167-
[dir='rtl'] :is(.dark .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
169+
:where([dir='rtl'])
170+
:is(:where(.dark) .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
168171
)::file-selector-button:hover {
169172
background-color: #000;
170173
}
@@ -193,7 +196,7 @@ test('pseudo-elements are appended after the `:-webkit-any()`', () => {
193196
return run(input, config).then((result) => {
194197
expect(result.css).toMatchFormattedCss(css`
195198
${defaults}
196-
#app :is(.dark .dark\:before\:flex):before {
199+
#app :is(:where(.dark) .dark\:before\:flex):before {
197200
content: var(--tw-content);
198201
display: flex;
199202
}

0 commit comments

Comments
 (0)