diff --git a/CHANGELOG.md b/CHANGELOG.md index 4371274c84cd..22e81b2bffb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don't remove keyframe stops when using important utilities ([#12639](https://github.com/tailwindlabs/tailwindcss/pull/12639)) - Don't add spaces to gradients and grid track names when followed by `calc()` ([#12704](https://github.com/tailwindlabs/tailwindcss/pull/12704)) +- Restore old behavior for `class` dark mode strategy ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717)) - Improve glob handling for folders with `(`, `)`, `[` or `]` in the file path ([#12715](https://github.com/tailwindlabs/tailwindcss/pull/12715)) ### Added +- Add new `selector` and `variant` strategies for dark mode ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717)) - [Oxide] New Rust template parsing engine ([#10252](https://github.com/tailwindlabs/tailwindcss/pull/10252)) - [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))) - [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 ### Changed +- Support `rtl` and `ltr` variants on same element as `dir` attribute ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717)) - [Oxide] Deprecate `--no-autoprefixer` flag in the CLI ([#11280](https://github.com/tailwindlabs/tailwindcss/pull/11280)) - [Oxide] Make the Rust based parser the default ([#11394](https://github.com/tailwindlabs/tailwindcss/pull/11394)) diff --git a/src/corePlugins.js b/src/corePlugins.js index 6b2f88ffcf82..7e04044308fe 100644 --- a/src/corePlugins.js +++ b/src/corePlugins.js @@ -206,8 +206,8 @@ export let variantPlugins = { }, directionVariants: ({ addVariant }) => { - addVariant('ltr', ':is(:where([dir="ltr"]) &)') - addVariant('rtl', ':is(:where([dir="rtl"]) &)') + addVariant('ltr', '&:where([dir="ltr"], [dir="ltr"] *)') + addVariant('rtl', '&:where([dir="rtl"], [dir="rtl"] *)') }, reducedMotionVariants: ({ addVariant }) => { @@ -216,7 +216,7 @@ export let variantPlugins = { }, darkVariants: ({ config, addVariant }) => { - let [mode, className = '.dark'] = [].concat(config('darkMode', 'media')) + let [mode, selector = '.dark'] = [].concat(config('darkMode', 'media')) if (mode === false) { mode = 'media' @@ -227,10 +227,49 @@ export let variantPlugins = { ]) } - if (mode === 'class') { - addVariant('dark', `:is(:where(${className}) &)`) + if (mode === 'variant') { + let formats + if (Array.isArray(selector)) { + formats = selector + } else if (typeof selector === 'function') { + formats = selector + } else if (typeof selector === 'string') { + formats = [selector] + } + + // TODO: We could also add these warnings if the user passes a function that returns string | string[] + // But this is an advanced enough use case that it's probably not necessary + if (Array.isArray(formats)) { + for (let format of formats) { + if (format === '.dark') { + mode = false + log.warn('darkmode-variant-without-selector', [ + 'When using `variant` for `darkMode`, you must provide a selector.', + 'Example: `darkMode: ["variant", ".your-selector &"]`', + ]) + } else if (!format.includes('&')) { + mode = false + log.warn('darkmode-variant-without-ampersand', [ + 'When using `variant` for `darkMode`, your selector must contain `&`.', + 'Example `darkMode: ["variant", ".your-selector &"]`', + ]) + } + } + } + + selector = formats + } + + if (mode === 'selector') { + // New preferred behavior + addVariant('dark', `&:where(${selector}, ${selector} *)`) } else if (mode === 'media') { addVariant('dark', '@media (prefers-color-scheme: dark)') + } else if (mode === 'variant') { + addVariant('dark', selector) + } else if (mode === 'class') { + // Old behavior + addVariant('dark', `:is(${selector} &)`) } }, diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index e677399b8b8e..d6380211b933 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -757,14 +757,35 @@ function resolvePlugins(context, root) { variantPlugins['supportsVariants'], variantPlugins['reducedMotionVariants'], variantPlugins['prefersContrastVariants'], - variantPlugins['printVariant'], variantPlugins['screenVariants'], variantPlugins['orientationVariants'], variantPlugins['directionVariants'], variantPlugins['darkVariants'], variantPlugins['forcedColorsVariants'], + variantPlugins['printVariant'], ] + // This is a compatibility fix for the pre 3.4 dark mode behavior + // `class` retains the old behavior, but `selector` keeps the new behavior + let isLegacyDarkMode = + context.tailwindConfig.darkMode === 'class' || + (Array.isArray(context.tailwindConfig.darkMode) && + context.tailwindConfig.darkMode[0] === 'class') + + if (isLegacyDarkMode) { + afterVariants = [ + variantPlugins['supportsVariants'], + variantPlugins['reducedMotionVariants'], + variantPlugins['prefersContrastVariants'], + variantPlugins['darkVariants'], + variantPlugins['screenVariants'], + variantPlugins['orientationVariants'], + variantPlugins['directionVariants'], + variantPlugins['forcedColorsVariants'], + variantPlugins['printVariant'], + ] + } + return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins] } diff --git a/src/util/pseudoElements.js b/src/util/pseudoElements.js index 5795cdd42045..e518801f42ba 100644 --- a/src/util/pseudoElements.js +++ b/src/util/pseudoElements.js @@ -60,6 +60,10 @@ let elementProperties = { ':first-letter': ['terminal', 'jumpable'], ':first-line': ['terminal', 'jumpable'], + ':where': [], + ':is': [], + ':has': [], + // The default value is used when the pseudo-element is not recognized // Because it's not recognized, we don't know if it's terminal or not // So we assume it can be moved AND can have user-action pseudo classes attached to it diff --git a/tests/apply.test.js b/tests/apply.test.js index 6c79db2b74e1..1415019e4acb 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -34,7 +34,7 @@ let sharedHtml = html` test('@apply', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: sharedHtml }], } @@ -215,14 +215,14 @@ test('@apply', () => { text-align: left; } } - :is(:where(.dark) .apply-dark-variant) { + .apply-dark-variant:where(.dark, .dark *) { text-align: center; } - :is(:where(.dark) .apply-dark-variant:hover) { + .apply-dark-variant:hover:where(.dark, .dark *) { text-align: right; } @media (min-width: 1024px) { - :is(:where(.dark) .apply-dark-variant) { + .apply-dark-variant:where(.dark, .dark *) { text-align: left; } } @@ -452,7 +452,7 @@ test('@apply', () => { test('@apply error with unknown utility', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: sharedHtml }], } @@ -472,7 +472,7 @@ test('@apply error with unknown utility', async () => { test('@apply error with nested @screen', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: sharedHtml }], } @@ -496,7 +496,7 @@ test('@apply error with nested @screen', async () => { test('@apply error with nested @anyatrulehere', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: sharedHtml }], } @@ -520,7 +520,7 @@ test('@apply error with nested @anyatrulehere', async () => { test('@apply error when using .group utility', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: '
' }], } @@ -543,7 +543,7 @@ test('@apply error when using .group utility', async () => { test('@apply error when using a prefixed .group utility', async () => { let config = { prefix: 'tw-', - darkMode: 'class', + darkMode: 'selector', content: [{ raw: html`
` }], } @@ -565,7 +565,7 @@ test('@apply error when using a prefixed .group utility', async () => { test('@apply error when using .peer utility', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: '
' }], } @@ -588,7 +588,7 @@ test('@apply error when using .peer utility', async () => { test('@apply error when using a prefixed .peer utility', async () => { let config = { prefix: 'tw-', - darkMode: 'class', + darkMode: 'selector', content: [{ raw: html`
` }], } @@ -1972,7 +1972,7 @@ it('should maintain the correct selector when applying other utilities', () => { it('pseudo elements inside apply are moved outside of :is() or :has()', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, @@ -2016,28 +2016,30 @@ it('pseudo elements inside apply are moved outside of :is() or :has()', () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` - :is(:where(.dark) .foo):before, - :is(:where([dir='rtl']) :is(:where(.dark) .bar)):before, - :is(:where([dir='rtl']) :is(:where(.dark) .baz:hover)):before { + .foo:where(.dark, .dark *):before, + .bar:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *):before, + .baz:hover:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *):before { background-color: #000; } - :-webkit-any( - :where([dir='rtl']) :-webkit-any(:where(.dark) .qux) + .qux:where(.dark, .dark *):where( + [dir='rtl'], + [dir='rtl'] * )::-webkit-file-upload-button:hover { background-color: #000; } - :is(:where([dir='rtl']) :is(:where(.dark) .qux))::file-selector-button:hover { + .qux:where(.dark, .dark *):where([dir='rtl'], [dir='rtl'] *)::file-selector-button:hover { background-color: #000; } - :is(:where([dir='rtl']) :is(:where(.dark) .steve):hover):before { + .steve:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *):before { background-color: #000; } - :-webkit-any( - :where([dir='rtl']) :-webkit-any(:where(.dark) .bob) - )::-webkit-file-upload-button:hover { + .bob:where(.dark, .dark *):hover:where( + [dir='rtl'], + [dir='rtl'] * + )::-webkit-file-upload-button { background-color: #000; } - :is(:where([dir='rtl']) :is(:where(.dark) .bob))::file-selector-button:hover { + .bob:where(.dark, .dark *):hover:where([dir='rtl'], [dir='rtl'] *)::file-selector-button { background-color: #000; } :has([dir='rtl'] .foo:hover):before { @@ -2055,7 +2057,7 @@ it('pseudo elements inside apply are moved outside of :is() or :has()', () => { test('::ng-deep, ::deep, ::v-deep pseudo elements are left alone', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, diff --git a/tests/custom-separator.test.js b/tests/custom-separator.test.js index a839c547942b..61b41ddf167d 100644 --- a/tests/custom-separator.test.js +++ b/tests/custom-separator.test.js @@ -2,7 +2,7 @@ import { run, html, css } from './util/run' test('custom separator', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` @@ -32,10 +32,10 @@ test('custom separator', () => { text-align: right; } } - :is(:where([dir='rtl']) .rtl_active_text-center:active) { + .rtl_active_text-center:active:where([dir='rtl'], [dir='rtl'] *) { text-align: center; } - :is(:where(.dark) .dark_focus_text-left:focus) { + .dark_focus_text-left:focus:where(.dark, .dark *) { text-align: left; } `) @@ -44,7 +44,7 @@ test('custom separator', () => { test('dash is not supported', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: 'lg-hover-font-bold' }], separator: '-', } diff --git a/tests/dark-mode.test.js b/tests/dark-mode.test.js index 276c6c9e3c87..342a3939ad3e 100644 --- a/tests/dark-mode.test.js +++ b/tests/dark-mode.test.js @@ -1,8 +1,8 @@ import { run, html, css, defaults } from './util/run' -it('should be possible to use the darkMode "class" mode', () => { +it('should be possible to use the darkMode "selector" mode', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -16,16 +16,16 @@ it('should be possible to use the darkMode "class" mode', () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` ${defaults} - :is(:where(.dark) .dark\:font-bold) { + .dark\:font-bold:where(.dark, .dark *) { font-weight: 700; } `) }) }) -it('should be possible to change the class name', () => { +it('should be possible to change the selector', () => { let config = { - darkMode: ['class', '.test-dark'], + darkMode: ['selector', '.test-dark'], content: [{ raw: html`
` }], corePlugins: { preflight: false }, } @@ -39,7 +39,7 @@ it('should be possible to change the class name', () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` ${defaults} - :is(:where(.test-dark) .dark\:font-bold) { + .dark\:font-bold:where(.test-dark, .test-dark *) { font-weight: 700; } `) @@ -119,3 +119,175 @@ it('should default to the `media` mode when mode is set to `false`', () => { `) }) }) + +it('should support the deprecated `class` dark mode behavior', () => { + let config = { + darkMode: 'class', + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + :is(.dark .dark\:font-bold) { + font-weight: 700; + } + `) + }) +}) + +it('should support custom classes with deprecated `class` dark mode', () => { + let config = { + darkMode: ['class', '.my-dark'], + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + :is(.my-dark .dark\:font-bold) { + font-weight: 700; + } + `) + }) +}) + +it('should use legacy sorting when using `darkMode: class`', () => { + let config = { + darkMode: 'class', + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .hover\:text-green-200:hover { + --tw-text-opacity: 1; + color: rgb(187 247 208 / var(--tw-text-opacity)); + } + :is(.dark .dark\:text-green-100) { + --tw-text-opacity: 1; + color: rgb(220 252 231 / var(--tw-text-opacity)); + } + @media (min-width: 1024px) { + .lg\:text-green-300 { + --tw-text-opacity: 1; + color: rgb(134 239 172 / var(--tw-text-opacity)); + } + } + `) + }) +}) + +it('should use modern sorting otherwise', () => { + let config = { + darkMode: 'selector', + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .hover\:text-green-200:hover { + --tw-text-opacity: 1; + color: rgb(187 247 208 / var(--tw-text-opacity)); + } + @media (min-width: 1024px) { + .lg\:text-green-300 { + --tw-text-opacity: 1; + color: rgb(134 239 172 / var(--tw-text-opacity)); + } + } + .dark\:text-green-100:where(.dark, .dark *) { + --tw-text-opacity: 1; + color: rgb(220 252 231 / var(--tw-text-opacity)); + } + `) + }) +}) + +it('should allow customization of the dark mode variant', () => { + let config = { + darkMode: ['variant', '&:not(.light *)'], + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .dark\:font-bold:not(.light *) { + font-weight: 700; + } + `) + }) +}) + +it('should support parallel selectors for the dark mode variant', () => { + let config = { + darkMode: ['variant', ['&:not(.light *)', '&:not(.extralight *)']], + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .dark\:font-bold:not(.light *), + .dark\:font-bold:not(.extralight *) { + font-weight: 700; + } + `) + }) +}) + +it('should support fn selectors for the dark mode variant', () => { + let config = { + darkMode: ['variant', () => ['&:not(.light *)', '&:not(.extralight *)']], + content: [{ raw: html`
` }], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .dark\:font-bold:not(.light *), + .dark\:font-bold:not(.extralight *) { + font-weight: 700; + } + `) + }) +}) diff --git a/tests/import-processing-c.js b/tests/import-processing-c.js index 164503d5ec5d..3bfef1ae395a 100644 --- a/tests/import-processing-c.js +++ b/tests/import-processing-c.js @@ -1,5 +1,5 @@ module.exports = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: `
`, diff --git a/tests/import-processing.test.js b/tests/import-processing.test.js index ef59be3486e9..ad14d9bcfdbc 100644 --- a/tests/import-processing.test.js +++ b/tests/import-processing.test.js @@ -3,7 +3,7 @@ import { html, css, run } from './util/run' describe('import processing', () => { it('should be possible to import another css file', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, @@ -27,7 +27,7 @@ describe('import processing', () => { it('should be possible to import another css file after @tailwind directive', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, diff --git a/tests/important-boolean.test.js b/tests/important-boolean.test.js index ec49b42439f5..116d97ec8ff6 100644 --- a/tests/important-boolean.test.js +++ b/tests/important-boolean.test.js @@ -7,7 +7,7 @@ import { run, html, css, defaults } from './util/run' test('important boolean', () => { let config = { important: true, - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` @@ -147,10 +147,10 @@ test('important boolean', () => { text-align: right !important; } } - :is(:where([dir='rtl']) .rtl\:active\:text-center:active) { + .rtl\:active\:text-center:active:where([dir='rtl'], [dir='rtl'] *) { text-align: center !important; } - :is(:where(.dark) .dark\:focus\:text-left:focus) { + .dark\:focus\:text-left:focus:where(.dark, .dark *) { text-align: left !important; } `) diff --git a/tests/important-modifier-prefix.test.js b/tests/important-modifier-prefix.test.js index 2c6a8bb62642..3c34d4f2a99c 100644 --- a/tests/important-modifier-prefix.test.js +++ b/tests/important-modifier-prefix.test.js @@ -4,7 +4,7 @@ test('important modifier with prefix', () => { let config = { important: false, prefix: 'tw-', - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` diff --git a/tests/important-modifier.test.js b/tests/important-modifier.test.js index da5ac5d7d4e6..57177a8c3408 100644 --- a/tests/important-modifier.test.js +++ b/tests/important-modifier.test.js @@ -3,7 +3,7 @@ import { run, html, css } from './util/run' test('important modifier', () => { let config = { important: false, - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` diff --git a/tests/important-selector.test.js b/tests/important-selector.test.js index aea4f1547ca5..f3d31a0276b1 100644 --- a/tests/important-selector.test.js +++ b/tests/important-selector.test.js @@ -3,7 +3,7 @@ import { run, html, css, defaults } from './util/run' test('important selector', () => { let config = { important: '#app', - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` @@ -145,30 +145,28 @@ test('important selector', () => { text-align: right; } } - #app :is(:where([dir='rtl']) .rtl\:active\:text-center:active) { + #app .rtl\:active\:text-center:active:where([dir='rtl'], [dir='rtl'] *) { text-align: center; } - #app :is(:where(.dark) .dark\:before\:underline):before { + #app .dark\:before\:underline:where(.dark, .dark *):before { content: var(--tw-content); text-decoration-line: underline; } - #app :is(:where(.dark) .dark\:focus\:text-left:focus) { + #app .dark\:focus\:text-left:focus:where(.dark, .dark *) { text-align: left; } #app - :-webkit-any( - :where([dir='rtl']) - :-webkit-any( - :where(.dark) .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100 - ) - )::-webkit-file-upload-button:hover { + .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100:where( + .dark, + .dark * + ):where([dir='rtl'], [dir='rtl'] *)::-webkit-file-upload-button:hover { background-color: #000; } #app - :is( - :where([dir='rtl']) - :is(:where(.dark) .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100) - )::file-selector-button:hover { + .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100:where( + .dark, + .dark * + ):where([dir='rtl'], [dir='rtl'] *)::file-selector-button:hover { background-color: #000; } `) @@ -178,7 +176,7 @@ test('important selector', () => { test('pseudo-elements are appended after the `:-webkit-any()`', () => { let config = { important: '#app', - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, @@ -196,7 +194,7 @@ test('pseudo-elements are appended after the `:-webkit-any()`', () => { return run(input, config).then((result) => { expect(result.css).toMatchFormattedCss(css` ${defaults} - #app :is(:where(.dark) .dark\:before\:flex):before { + #app .dark\:before\:flex:where(.dark, .dark *):before { content: var(--tw-content); display: flex; } diff --git a/tests/kitchen-sink.test.js b/tests/kitchen-sink.test.js index 2151f22db952..a869ad4c8f33 100644 --- a/tests/kitchen-sink.test.js +++ b/tests/kitchen-sink.test.js @@ -2,7 +2,7 @@ import { run, html, css, defaults } from './util/run' test('it works', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` @@ -304,7 +304,7 @@ test('it works', () => { } .drop-empty-rules:hover, .group:hover .apply-group, - :is(:where(.dark) .apply-dark-mode) { + .apply-dark-mode:where(.dark, .dark *) { font-weight: 700; } .apply-with-existing:hover { @@ -339,7 +339,7 @@ test('it works', () => { .apply-order-b { margin: 1.5rem 1.25rem 1.25rem; } - :is(:where(.dark) .group:hover .apply-dark-group-example-a) { + .group:hover .apply-dark-group-example-a:where(.dark, .dark *) { --tw-bg-opacity: 1; background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } @@ -806,12 +806,12 @@ test('it works', () => { text-align: left; } } - :is(:where(.dark) .dark\:custom-util) { + .dark\:custom-util:where(.dark, .dark *) { background: #abcdef; } @media (min-width: 768px) { @media (prefers-reduced-motion: no-preference) { - :is(:where(.dark) .md\:dark\:motion-safe\:foo\:active\:custom-util:active) { + .md\:dark\:motion-safe\:foo\:active\:custom-util:active:where(.dark, .dark *) { background: #abcdef !important; } } diff --git a/tests/modify-selectors.test.js b/tests/modify-selectors.test.js index e6ce3cf147c6..364b84a7ec7e 100644 --- a/tests/modify-selectors.test.js +++ b/tests/modify-selectors.test.js @@ -4,7 +4,7 @@ import { run, html, css } from './util/run' test('modify selectors', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` diff --git a/tests/opacity.test.js b/tests/opacity.test.js index 04b23aea783c..8f688210cead 100644 --- a/tests/opacity.test.js +++ b/tests/opacity.test.js @@ -2,7 +2,7 @@ import { run, html, css } from './util/run' test('opacity', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` @@ -42,7 +42,7 @@ test('opacity', () => { test('colors defined as functions work when opacity plugins are disabled', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` diff --git a/tests/plugins/variants/__snapshots__/darkVariants.test.js.snap b/tests/plugins/variants/__snapshots__/darkVariants.test.js.snap index 5414de562dbf..771a41d19d4a 100644 --- a/tests/plugins/variants/__snapshots__/darkVariants.test.js.snap +++ b/tests/plugins/variants/__snapshots__/darkVariants.test.js.snap @@ -12,7 +12,7 @@ exports[`should test the 'darkVariants' plugin 1`] = ` exports[`should test the 'darkVariants' plugin 2`] = ` " -:is(:where(.dark) .dark\\:flex) { +:is(.dark .dark\\:flex) { display: flex; } " @@ -20,7 +20,23 @@ exports[`should test the 'darkVariants' plugin 2`] = ` exports[`should test the 'darkVariants' plugin 3`] = ` " -:is(:where(.my-dark-mode) .dark\\:flex) { +:is(.my-dark-mode .dark\\:flex) { + display: flex; +} +" +`; + +exports[`should test the 'darkVariants' plugin 4`] = ` +" +.dark\\:flex:where(.dark, .dark *) { + display: flex; +} +" +`; + +exports[`should test the 'darkVariants' plugin 5`] = ` +" +.dark\\:flex:where(.my-dark-mode, .my-dark-mode *) { display: flex; } " diff --git a/tests/plugins/variants/__snapshots__/directionVariants.test.js.snap b/tests/plugins/variants/__snapshots__/directionVariants.test.js.snap index 3f3f79cf96ed..840ff25b0618 100644 --- a/tests/plugins/variants/__snapshots__/directionVariants.test.js.snap +++ b/tests/plugins/variants/__snapshots__/directionVariants.test.js.snap @@ -2,7 +2,7 @@ exports[`should test the 'directionVariants' plugin 1`] = ` " -:is(:where([dir="ltr"]) .ltr\\:flex), :is(:where([dir="rtl"]) .rtl\\:flex) { +.ltr\\:flex:where([dir="ltr"], [dir="ltr"] *), .rtl\\:flex:where([dir="rtl"], [dir="rtl"] *) { display: flex; } " diff --git a/tests/plugins/variants/darkVariants.test.js b/tests/plugins/variants/darkVariants.test.js index 2080df0880ec..c13ceb5b1f77 100644 --- a/tests/plugins/variants/darkVariants.test.js +++ b/tests/plugins/variants/darkVariants.test.js @@ -12,3 +12,13 @@ quickVariantPluginTest('darkVariants', { quickVariantPluginTest('darkVariants', { darkMode: ['class', '.my-dark-mode'], }).toMatchSnapshot() + +// Selector dark mode +quickVariantPluginTest('darkVariants', { + darkMode: 'selector', +}).toMatchSnapshot() + +// Selector dark mode with custom selector +quickVariantPluginTest('darkVariants', { + darkMode: ['selector', '.my-dark-mode'], +}).toMatchSnapshot() diff --git a/tests/prefix.test.js b/tests/prefix.test.js index a0ce18cbd5fa..ab580d192145 100644 --- a/tests/prefix.test.js +++ b/tests/prefix.test.js @@ -3,7 +3,7 @@ import { run, html, css, defaults } from './util/run' test('prefix', () => { let config = { prefix: 'tw-', - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html` @@ -126,7 +126,7 @@ test('prefix', () => { .custom-component { font-weight: 700; } - :is(:where(.tw-dark) .tw-group:hover .custom-component) { + .tw-group:hover .custom-component:where(.tw-dark, .tw-dark *) { font-weight: 400; } .tw--ml-4 { @@ -169,14 +169,14 @@ test('prefix', () => { text-align: right; } } - :is(:where([dir='rtl']) .rtl\:active\:tw-text-center:active) { + .rtl\:active\:tw-text-center:active:where([dir='rtl'], [dir='rtl'] *) { text-align: center; } - :is(:where(.tw-dark) .dark\:tw-bg-\[rgb\(255\,0\,0\)\]) { + .dark\:tw-bg-\[rgb\(255\,0\,0\)\]:where(.tw-dark, .tw-dark *) { --tw-bg-opacity: 1; background-color: rgb(255 0 0 / var(--tw-bg-opacity)); } - :is(:where(.tw-dark) .dark\:focus\:tw-text-left:focus) { + .dark\:focus\:tw-text-left:focus:where(.tw-dark, .tw-dark *) { text-align: left; } `) diff --git a/tests/variants.test.css b/tests/variants.test.css index bf3275257dc9..b8e87257cf79 100644 --- a/tests/variants.test.css +++ b/tests/variants.test.css @@ -293,11 +293,6 @@ display: flex; } } -@media print { - .print\:flex { - display: flex; - } -} @media (min-width: 640px) { .sm\:flex, .sm\:active\:flex:active { @@ -335,28 +330,28 @@ display: flex; } } -:is(:where([dir='ltr']) .ltr\:flex), -:is(:where([dir='rtl']) .rtl\:flex), -:is(:where(.dark) .dark\:flex), -:is( - :where(.dark) .group:disabled:focus:hover .dark\:group-disabled\:group-focus\:group-hover\:flex - ), -:is(:where(.dark) .peer:disabled:focus:hover ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:flex) { +.ltr\:flex:where([dir='ltr'], [dir='ltr'] *), +.rtl\:flex:where([dir='rtl'], [dir='rtl'] *), +.dark\:flex:where(.dark, .dark *), +.group:disabled:focus:hover + .dark\:group-disabled\:group-focus\:group-hover\:flex:where(.dark, .dark *), +.peer:disabled:focus:hover + ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:flex:where(.dark, .dark *) { display: flex; } @media (min-width: 1024px) { - :is(:where(.dark) .lg\:dark\:flex) { + .lg\:dark\:flex:where(.dark, .dark *) { display: flex; } } @media (min-width: 1280px) { - :is(:where(.dark) .xl\:dark\:disabled\:flex:disabled) { + .xl\:dark\:disabled\:flex:disabled:where(.dark, .dark *) { display: flex; } } @media (min-width: 1536px) { @media (prefers-reduced-motion: no-preference) { - :is(:where(.dark) .\32 xl\:dark\:motion-safe\:focus-within\:flex:focus-within) { + .\32 xl\:dark\:motion-safe\:focus-within\:flex:focus-within:where(.dark, .dark *) { display: flex; } } @@ -366,3 +361,8 @@ display: flex; } } +@media print { + .print\:flex { + display: flex; + } +} diff --git a/tests/variants.test.js b/tests/variants.test.js index 513b950a6238..bccebf8a593f 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -5,7 +5,7 @@ import { run, html, css, defaults } from './util/run' test('variants', () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [path.resolve(__dirname, './variants.test.html')], corePlugins: { preflight: false }, } @@ -1122,7 +1122,7 @@ test('arbitrary variant selectors should not re-order scrollbar pseudo classes', test('stacking dark and rtl variants', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, @@ -1138,7 +1138,7 @@ test('stacking dark and rtl variants', async () => { let result = await run(input, config) expect(result.css).toMatchFormattedCss(css` - :is(:where(.dark) :is(:where([dir='rtl']) .dark\:rtl\:italic)) { + .dark\:rtl\:italic:where([dir='rtl'], [dir='rtl'] *):where(.dark, .dark *) { font-style: italic; } `) @@ -1146,7 +1146,7 @@ test('stacking dark and rtl variants', async () => { test('stacking dark and rtl variants with pseudo elements', async () => { let config = { - darkMode: 'class', + darkMode: 'selector', content: [ { raw: html`
`, @@ -1162,7 +1162,10 @@ test('stacking dark and rtl variants with pseudo elements', async () => { let result = await run(input, config) expect(result.css).toMatchFormattedCss(css` - :is(:where(.dark) :is(:where([dir='rtl']) .dark\:rtl\:placeholder\:italic))::placeholder { + .dark\:rtl\:placeholder\:italic:where([dir='rtl'], [dir='rtl'] *):where( + .dark, + .dark * + )::placeholder { font-style: italic; } `) diff --git a/types/config.d.ts b/types/config.d.ts index 051ccad5b47f..f0c342a78569 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -75,6 +75,13 @@ type DarkModeConfig = | 'class' // Use the `class` strategy with a custom class instead of `.dark`. | ['class', string] + // Use the `selector` strategy — same as `class` but uses `:where()` for more predicable behavior + | 'selector' + // Use the `selector` strategy with a custom selector instead of `.dark`. + | ['selector', string] + // Use the `variant` strategy, which allows you to completely customize the selector + // It takes a string or an array of strings, which are passed directly to `addVariant()` + | ['variant', string | string[]] type Screen = { raw: string } | { min: string } | { max: string } | { min: string; max: string } type ScreensConfig = string[] | KeyValuePair