Skip to content

Commit fb1743b

Browse files
Restore old behavior for class dark mode, add new selector and variant options for dark mode (#12717)
* Add dark mode variant option * Tweak warning messages * Add legacy dark mode option * wip * Use `class` for legacy behavior, `selector` for new behavior * Add simplified failing apply/where test case * Switch to `where` list, apply changes to `dir` variants * Don’t let `:where`, `:is:`, or `:has` be attached to pseudo elements * Updating tests... * Finish updating tests * Remove `variant` dark mode strategy * Update types * Update comments * Update changelog * Revert "Remove `variant` dark mode strategy" This reverts commit 1852504. * Add variant back to types * wip * Update comments * Update tests * Rename variable * Update changelog * Update changelog * Update changelog * Fix CS --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent a5ae318 commit fb1743b

23 files changed

+375
-100
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Don't remove keyframe stops when using important utilities ([#12639](https://github.com/tailwindlabs/tailwindcss/pull/12639))
1313
- Don't add spaces to gradients and grid track names when followed by `calc()` ([#12704](https://github.com/tailwindlabs/tailwindcss/pull/12704))
14+
- Restore old behavior for `class` dark mode strategy ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
1415
- Improve glob handling for folders with `(`, `)`, `[` or `]` in the file path ([#12715](https://github.com/tailwindlabs/tailwindcss/pull/12715))
1516

1617
### Added
1718

19+
- Add new `selector` and `variant` strategies for dark mode ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
1820
- [Oxide] New Rust template parsing engine ([#10252](https://github.com/tailwindlabs/tailwindcss/pull/10252))
1921
- [Oxide] Support `@import "tailwindcss"` using top-level `index.css` file ([#11205](https://github.com/tailwindlabs/tailwindcss/pull/11205), ([#11260](https://github.com/tailwindlabs/tailwindcss/pull/11260)))
2022
- [Oxide] Use `lightningcss` for nesting and vendor prefixes in PostCSS plugin ([#10399](https://github.com/tailwindlabs/tailwindcss/pull/10399))
@@ -25,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2527

2628
### Changed
2729

30+
- Support `rtl` and `ltr` variants on same element as `dir` attribute ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
2831
- [Oxide] Deprecate `--no-autoprefixer` flag in the CLI ([#11280](https://github.com/tailwindlabs/tailwindcss/pull/11280))
2932
- [Oxide] Make the Rust based parser the default ([#11394](https://github.com/tailwindlabs/tailwindcss/pull/11394))
3033

src/corePlugins.js

+44-5
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ export let variantPlugins = {
206206
},
207207

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

213213
reducedMotionVariants: ({ addVariant }) => {
@@ -216,7 +216,7 @@ export let variantPlugins = {
216216
},
217217

218218
darkVariants: ({ config, addVariant }) => {
219-
let [mode, className = '.dark'] = [].concat(config('darkMode', 'media'))
219+
let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media'))
220220

221221
if (mode === false) {
222222
mode = 'media'
@@ -227,10 +227,49 @@ export let variantPlugins = {
227227
])
228228
}
229229

230-
if (mode === 'class') {
231-
addVariant('dark', `:is(:where(${className}) &)`)
230+
if (mode === 'variant') {
231+
let formats
232+
if (Array.isArray(selector)) {
233+
formats = selector
234+
} else if (typeof selector === 'function') {
235+
formats = selector
236+
} else if (typeof selector === 'string') {
237+
formats = [selector]
238+
}
239+
240+
// TODO: We could also add these warnings if the user passes a function that returns string | string[]
241+
// But this is an advanced enough use case that it's probably not necessary
242+
if (Array.isArray(formats)) {
243+
for (let format of formats) {
244+
if (format === '.dark') {
245+
mode = false
246+
log.warn('darkmode-variant-without-selector', [
247+
'When using `variant` for `darkMode`, you must provide a selector.',
248+
'Example: `darkMode: ["variant", ".your-selector &"]`',
249+
])
250+
} else if (!format.includes('&')) {
251+
mode = false
252+
log.warn('darkmode-variant-without-ampersand', [
253+
'When using `variant` for `darkMode`, your selector must contain `&`.',
254+
'Example `darkMode: ["variant", ".your-selector &"]`',
255+
])
256+
}
257+
}
258+
}
259+
260+
selector = formats
261+
}
262+
263+
if (mode === 'selector') {
264+
// New preferred behavior
265+
addVariant('dark', `&:where(${selector}, ${selector} *)`)
232266
} else if (mode === 'media') {
233267
addVariant('dark', '@media (prefers-color-scheme: dark)')
268+
} else if (mode === 'variant') {
269+
addVariant('dark', selector)
270+
} else if (mode === 'class') {
271+
// Old behavior
272+
addVariant('dark', `:is(${selector} &)`)
234273
}
235274
},
236275

src/lib/setupContextUtils.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -757,14 +757,35 @@ function resolvePlugins(context, root) {
757757
variantPlugins['supportsVariants'],
758758
variantPlugins['reducedMotionVariants'],
759759
variantPlugins['prefersContrastVariants'],
760-
variantPlugins['printVariant'],
761760
variantPlugins['screenVariants'],
762761
variantPlugins['orientationVariants'],
763762
variantPlugins['directionVariants'],
764763
variantPlugins['darkVariants'],
765764
variantPlugins['forcedColorsVariants'],
765+
variantPlugins['printVariant'],
766766
]
767767

768+
// This is a compatibility fix for the pre 3.4 dark mode behavior
769+
// `class` retains the old behavior, but `selector` keeps the new behavior
770+
let isLegacyDarkMode =
771+
context.tailwindConfig.darkMode === 'class' ||
772+
(Array.isArray(context.tailwindConfig.darkMode) &&
773+
context.tailwindConfig.darkMode[0] === 'class')
774+
775+
if (isLegacyDarkMode) {
776+
afterVariants = [
777+
variantPlugins['supportsVariants'],
778+
variantPlugins['reducedMotionVariants'],
779+
variantPlugins['prefersContrastVariants'],
780+
variantPlugins['darkVariants'],
781+
variantPlugins['screenVariants'],
782+
variantPlugins['orientationVariants'],
783+
variantPlugins['directionVariants'],
784+
variantPlugins['forcedColorsVariants'],
785+
variantPlugins['printVariant'],
786+
]
787+
}
788+
768789
return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
769790
}
770791

src/util/pseudoElements.js

+4
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ let elementProperties = {
6060
':first-letter': ['terminal', 'jumpable'],
6161
':first-line': ['terminal', 'jumpable'],
6262

63+
':where': [],
64+
':is': [],
65+
':has': [],
66+
6367
// The default value is used when the pseudo-element is not recognized
6468
// Because it's not recognized, we don't know if it's terminal or not
6569
// So we assume it can be moved AND can have user-action pseudo classes attached to it

tests/apply.test.js

+26-24
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ let sharedHtml = html`
3434

3535
test('@apply', () => {
3636
let config = {
37-
darkMode: 'class',
37+
darkMode: 'selector',
3838
content: [{ raw: sharedHtml }],
3939
}
4040

@@ -215,14 +215,14 @@ test('@apply', () => {
215215
text-align: left;
216216
}
217217
}
218-
:is(:where(.dark) .apply-dark-variant) {
218+
.apply-dark-variant:where(.dark, .dark *) {
219219
text-align: center;
220220
}
221-
:is(:where(.dark) .apply-dark-variant:hover) {
221+
.apply-dark-variant:hover:where(.dark, .dark *) {
222222
text-align: right;
223223
}
224224
@media (min-width: 1024px) {
225-
:is(:where(.dark) .apply-dark-variant) {
225+
.apply-dark-variant:where(.dark, .dark *) {
226226
text-align: left;
227227
}
228228
}
@@ -452,7 +452,7 @@ test('@apply', () => {
452452

453453
test('@apply error with unknown utility', async () => {
454454
let config = {
455-
darkMode: 'class',
455+
darkMode: 'selector',
456456
content: [{ raw: sharedHtml }],
457457
}
458458

@@ -472,7 +472,7 @@ test('@apply error with unknown utility', async () => {
472472

473473
test('@apply error with nested @screen', async () => {
474474
let config = {
475-
darkMode: 'class',
475+
darkMode: 'selector',
476476
content: [{ raw: sharedHtml }],
477477
}
478478

@@ -496,7 +496,7 @@ test('@apply error with nested @screen', async () => {
496496

497497
test('@apply error with nested @anyatrulehere', async () => {
498498
let config = {
499-
darkMode: 'class',
499+
darkMode: 'selector',
500500
content: [{ raw: sharedHtml }],
501501
}
502502

@@ -520,7 +520,7 @@ test('@apply error with nested @anyatrulehere', async () => {
520520

521521
test('@apply error when using .group utility', async () => {
522522
let config = {
523-
darkMode: 'class',
523+
darkMode: 'selector',
524524
content: [{ raw: '<div class="foo"></div>' }],
525525
}
526526

@@ -543,7 +543,7 @@ test('@apply error when using .group utility', async () => {
543543
test('@apply error when using a prefixed .group utility', async () => {
544544
let config = {
545545
prefix: 'tw-',
546-
darkMode: 'class',
546+
darkMode: 'selector',
547547
content: [{ raw: html`<div class="foo"></div>` }],
548548
}
549549

@@ -565,7 +565,7 @@ test('@apply error when using a prefixed .group utility', async () => {
565565

566566
test('@apply error when using .peer utility', async () => {
567567
let config = {
568-
darkMode: 'class',
568+
darkMode: 'selector',
569569
content: [{ raw: '<div class="foo"></div>' }],
570570
}
571571

@@ -588,7 +588,7 @@ test('@apply error when using .peer utility', async () => {
588588
test('@apply error when using a prefixed .peer utility', async () => {
589589
let config = {
590590
prefix: 'tw-',
591-
darkMode: 'class',
591+
darkMode: 'selector',
592592
content: [{ raw: html`<div class="foo"></div>` }],
593593
}
594594

@@ -1972,7 +1972,7 @@ it('should maintain the correct selector when applying other utilities', () => {
19721972

19731973
it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
19741974
let config = {
1975-
darkMode: 'class',
1975+
darkMode: 'selector',
19761976
content: [
19771977
{
19781978
raw: html` <div class="foo bar baz qux steve bob"></div> `,
@@ -2016,28 +2016,30 @@ 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(: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 {
2019+
.foo:where(.dark, .dark *):before,
2020+
.bar:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *):before,
2021+
.baz:hover:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *):before {
20222022
background-color: #000;
20232023
}
2024-
:-webkit-any(
2025-
:where([dir='rtl']) :-webkit-any(:where(.dark) .qux)
2024+
.qux:where(.dark, .dark *):where(
2025+
[dir='rtl'],
2026+
[dir='rtl'] *
20262027
)::-webkit-file-upload-button:hover {
20272028
background-color: #000;
20282029
}
2029-
:is(:where([dir='rtl']) :is(:where(.dark) .qux))::file-selector-button:hover {
2030+
.qux:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::file-selector-button:hover {
20302031
background-color: #000;
20312032
}
2032-
:is(:where([dir='rtl']) :is(:where(.dark) .steve):hover):before {
2033+
.steve:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *):before {
20332034
background-color: #000;
20342035
}
2035-
:-webkit-any(
2036-
:where([dir='rtl']) :-webkit-any(:where(.dark) .bob)
2037-
)::-webkit-file-upload-button:hover {
2036+
.bob:where(.dark, .dark *):hover:where(
2037+
[dir='rtl'],
2038+
[dir='rtl'] *
2039+
)::-webkit-file-upload-button {
20382040
background-color: #000;
20392041
}
2040-
:is(:where([dir='rtl']) :is(:where(.dark) .bob))::file-selector-button:hover {
2042+
.bob:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *)::file-selector-button {
20412043
background-color: #000;
20422044
}
20432045
:has([dir='rtl'] .foo:hover):before {
@@ -2055,7 +2057,7 @@ it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
20552057

20562058
test('::ng-deep, ::deep, ::v-deep pseudo elements are left alone', () => {
20572059
let config = {
2058-
darkMode: 'class',
2060+
darkMode: 'selector',
20592061
content: [
20602062
{
20612063
raw: html` <div class="foo bar"></div> `,

tests/custom-separator.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { run, html, css } from './util/run'
22

33
test('custom separator', () => {
44
let config = {
5-
darkMode: 'class',
5+
darkMode: 'selector',
66
content: [
77
{
88
raw: html`
@@ -32,10 +32,10 @@ test('custom separator', () => {
3232
text-align: right;
3333
}
3434
}
35-
:is(:where([dir='rtl']) .rtl_active_text-center:active) {
35+
.rtl_active_text-center:active:where([dir='rtl'], [dir='rtl'] *) {
3636
text-align: center;
3737
}
38-
:is(:where(.dark) .dark_focus_text-left:focus) {
38+
.dark_focus_text-left:focus:where(.dark, .dark *) {
3939
text-align: left;
4040
}
4141
`)
@@ -44,7 +44,7 @@ test('custom separator', () => {
4444

4545
test('dash is not supported', () => {
4646
let config = {
47-
darkMode: 'class',
47+
darkMode: 'selector',
4848
content: [{ raw: 'lg-hover-font-bold' }],
4949
separator: '-',
5050
}

0 commit comments

Comments
 (0)