From 3fb57e55abdba6fa68da6cb605fcaad5ce94764a Mon Sep 17 00:00:00 2001
From: Jordan Pittman <jordan@cryptica.me>
Date: Fri, 5 Jan 2024 14:39:34 -0500
Subject: [PATCH] Restore old behavior for `class` dark mode, add new
 `selector` and `variant` options for dark mode (#12717)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 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 185250438ccb2f61ba876d4676823c1807891122.

* 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 <4323180+adamwathan@users.noreply.github.com>
---
 CHANGELOG.md                                |   9 +
 src/corePlugins.js                          |  49 ++++-
 src/lib/setupContextUtils.js                |  23 ++-
 src/util/pseudoElements.js                  |   4 +
 tests/apply.test.js                         |  44 ++---
 tests/custom-separator.test.js              |   8 +-
 tests/dark-mode.test.js                     | 204 +++++++++++++++++++-
 tests/important-boolean.test.js             |   6 +-
 tests/important-modifier-prefix.test.js     |   2 +-
 tests/important-modifier.test.js            |   2 +-
 tests/important-selector.test.js            |  19 +-
 tests/kitchen-sink.test.js                  |  26 +--
 tests/modify-selectors.test.js              |   2 +-
 tests/opacity.test.js                       |   4 +-
 tests/prefix.test.js                        |  10 +-
 tests/util/apply-important-selector.test.js |  36 ++--
 tests/variants.oxide.test.css               |  54 ++++--
 tests/variants.test.css                     |  58 +++---
 tests/variants.test.js                      |  13 +-
 types/config.d.ts                           |   7 +
 20 files changed, 446 insertions(+), 134 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 993e3594fc2b..837728b7df24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,15 @@ 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))
+
+### Added
+
+- Add new `selector` and `variant` strategies for dark mode ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
+
+### Changed
+
+- Support `rtl` and `ltr` variants on same element as `dir` attribute ([#12717](https://github.com/tailwindlabs/tailwindcss/pull/12717))
 
 ## [3.4.0] - 2023-12-19
 
diff --git a/src/corePlugins.js b/src/corePlugins.js
index a04dce82e3bd..01afcec269d3 100644
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -207,8 +207,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 }) => {
@@ -217,7 +217,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'
@@ -228,10 +228,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 91d5f66bfc8e..72aa8f56aa68 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -767,14 +767,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 fe2b572496ba..ca5416e9261a 100644
--- a/tests/apply.test.js
+++ b/tests/apply.test.js
@@ -35,7 +35,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('@apply', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: sharedHtml }],
     }
 
@@ -216,14 +216,14 @@ crosscheck(({ stable, oxide }) => {
             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;
           }
         }
@@ -513,14 +513,14 @@ crosscheck(({ stable, oxide }) => {
             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;
           }
         }
@@ -755,7 +755,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('@apply error with unknown utility', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: sharedHtml }],
     }
 
@@ -775,7 +775,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('@apply error with nested @screen', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: sharedHtml }],
     }
 
@@ -799,7 +799,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('@apply error with nested @anyatrulehere', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: sharedHtml }],
     }
 
@@ -823,7 +823,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('@apply error when using .group utility', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: '<div class="foo"></div>' }],
     }
 
@@ -846,7 +846,7 @@ crosscheck(({ stable, oxide }) => {
   test('@apply error when using a prefixed .group utility', async () => {
     let config = {
       prefix: 'tw-',
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: html`<div class="foo"></div>` }],
     }
 
@@ -868,7 +868,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('@apply error when using .peer utility', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: '<div class="foo"></div>' }],
     }
 
@@ -891,7 +891,7 @@ crosscheck(({ stable, oxide }) => {
   test('@apply error when using a prefixed .peer utility', async () => {
     let config = {
       prefix: 'tw-',
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [{ raw: html`<div class="foo"></div>` }],
     }
 
@@ -2360,7 +2360,7 @@ crosscheck(({ stable, oxide }) => {
 
   it('pseudo elements inside apply are moved outside of :is() or :has()', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html` <div class="foo bar baz qux steve bob"></div> `,
@@ -2404,18 +2404,18 @@ crosscheck(({ stable, oxide }) => {
 
     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;
         }
-        :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;
         }
-        :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 {
@@ -2430,7 +2430,7 @@ crosscheck(({ stable, oxide }) => {
 
   stable.test('::ng-deep, ::deep, ::v-deep pseudo elements are left alone', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html` <div class="foo bar"></div> `,
diff --git a/tests/custom-separator.test.js b/tests/custom-separator.test.js
index 6546f4ed2c63..aee13e67e2f3 100644
--- a/tests/custom-separator.test.js
+++ b/tests/custom-separator.test.js
@@ -3,7 +3,7 @@ import { crosscheck, run, html, css } from './util/run'
 crosscheck(() => {
   test('custom separator', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
@@ -33,10 +33,10 @@ crosscheck(() => {
             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;
         }
       `)
@@ -45,7 +45,7 @@ crosscheck(() => {
 
   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 9845e0fdfe61..6fbea28d915b 100644
--- a/tests/dark-mode.test.js
+++ b/tests/dark-mode.test.js
@@ -1,6 +1,6 @@
 import { crosscheck, run, html, css, defaults } from './util/run'
 
-crosscheck(() => {
+crosscheck(({ oxide, stable }) => {
   it('should be possible to use the darkMode "class" mode', () => {
     let config = {
       darkMode: 'class',
@@ -17,7 +17,7 @@ crosscheck(() => {
     return run(input, config).then((result) => {
       expect(result.css).toMatchFormattedCss(css`
         ${defaults}
-        :is(:where(.dark) .dark\:font-bold) {
+        :is(.dark .dark\:font-bold) {
           font-weight: 700;
         }
       `)
@@ -40,7 +40,7 @@ crosscheck(() => {
     return run(input, config).then((result) => {
       expect(result.css).toMatchFormattedCss(css`
         ${defaults}
-        :is(:where(.test-dark) .dark\:font-bold) {
+        :is(.test-dark .dark\:font-bold) {
           font-weight: 700;
         }
       `)
@@ -120,4 +120,202 @@ crosscheck(() => {
       `)
     })
   })
+
+  it('should support the deprecated `class` dark mode behavior', () => {
+    let config = {
+      darkMode: 'class',
+      content: [{ raw: html`<div class="dark:font-bold"></div>` }],
+      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`<div class="dark:font-bold"></div>` }],
+      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`<div class="dark:text-green-100 hover:text-green-200 lg:text-green-300"></div>`,
+        },
+      ],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      @tailwind utilities;
+    `
+
+    return run(input, config).then((result) => {
+      stable.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));
+          }
+        }
+      `)
+      oxide.expect(result.css).toMatchFormattedCss(css`
+        .hover\:text-green-200:hover {
+          color: #bbf7d0;
+        }
+        :is(.dark .dark\:text-green-100) {
+          color: #dcfce7;
+        }
+        @media (min-width: 1024px) {
+          .lg\:text-green-300 {
+            color: #86efac;
+          }
+        }
+      `)
+    })
+  })
+
+  it('should use modern sorting otherwise', () => {
+    let config = {
+      darkMode: 'selector',
+      content: [
+        {
+          raw: html`<div class="dark:text-green-100 hover:text-green-200 lg:text-green-300"></div>`,
+        },
+      ],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      @tailwind utilities;
+    `
+
+    return run(input, config).then((result) => {
+      stable.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));
+        }
+      `)
+      oxide.expect(result.css).toMatchFormattedCss(css`
+        .hover\:text-green-200:hover {
+          color: #bbf7d0;
+        }
+        @media (min-width: 1024px) {
+          .lg\:text-green-300 {
+            color: #86efac;
+          }
+        }
+        .dark\:text-green-100:where(.dark, .dark *) {
+          color: #dcfce7;
+        }
+      `)
+    })
+  })
+
+  it('should allow customization of the dark mode variant', () => {
+    let config = {
+      darkMode: ['variant', '&:not(.light *)'],
+      content: [{ raw: html`<div class="dark:font-bold"></div>` }],
+      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`<div class="dark:font-bold"></div>` }],
+      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`<div class="dark:font-bold"></div>` }],
+      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/important-boolean.test.js b/tests/important-boolean.test.js
index 6b028736c441..4853f2a1f1db 100644
--- a/tests/important-boolean.test.js
+++ b/tests/important-boolean.test.js
@@ -8,7 +8,7 @@ crosscheck(() => {
   test('important boolean', () => {
     let config = {
       important: true,
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
@@ -148,10 +148,10 @@ crosscheck(() => {
             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 782ec809e417..1e9f2bd22d68 100644
--- a/tests/important-modifier-prefix.test.js
+++ b/tests/important-modifier-prefix.test.js
@@ -5,7 +5,7 @@ crosscheck(() => {
     let config = {
       important: false,
       prefix: 'tw-',
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`<!-- The string "!*" can cause problems if we don't handle it, let's include it -->
diff --git a/tests/important-modifier.test.js b/tests/important-modifier.test.js
index 6f6f1a8de54a..a4da55dd4ca5 100644
--- a/tests/important-modifier.test.js
+++ b/tests/important-modifier.test.js
@@ -4,7 +4,7 @@ crosscheck(() => {
   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 24533f02f34d..840c572864c6 100644
--- a/tests/important-selector.test.js
+++ b/tests/important-selector.test.js
@@ -4,7 +4,7 @@ crosscheck(({ stable, oxide }) => {
   test('important selector', () => {
     let config = {
       important: '#app',
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
@@ -146,19 +146,22 @@ crosscheck(({ stable, oxide }) => {
             text-align: right;
           }
         }
-        #app :is(:is(:where([dir='rtl']) .rtl\:active\:text-center:active)) {
+        #app :is(.rtl\:active\:text-center:active:where([dir='rtl'], [dir='rtl'] *)) {
           text-align: center;
         }
-        #app :is(:where(.dark) .dark\:before\:underline):before {
+        #app :is(.dark\:before\:underline:where(.dark, .dark *)):before {
           content: var(--tw-content);
           text-decoration-line: underline;
         }
-        #app :is(:is(:where(.dark) .dark\:focus\:text-left:focus)) {
+        #app :is(.dark\:focus\:text-left:focus:where(.dark, .dark *)) {
           text-align: left;
         }
         #app
           :is(
-            :where([dir='rtl']) :is(:where(.dark) .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100)
+            .hover\:\[\&\:\:file-selector-button\]\:rtl\:dark\:bg-black\/100:where(
+                .dark,
+                .dark *
+              ):where([dir='rtl'], [dir='rtl'] *)
           )::file-selector-button:hover {
           background-color: #000;
         }
@@ -169,7 +172,7 @@ crosscheck(({ stable, oxide }) => {
   test('pseudo-elements are appended after the `:is()`', () => {
     let config = {
       important: '#app',
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html` <div class="dark:before:bg-black"></div> `,
@@ -187,7 +190,7 @@ crosscheck(({ stable, oxide }) => {
     return run(input, config).then((result) => {
       stable.expect(result.css).toMatchFormattedCss(css`
         ${defaults}
-        #app :is(:where(.dark) .dark\:before\:bg-black)::before {
+        #app .dark\:before\:bg-black:where(.dark, .dark *)::before {
           content: var(--tw-content);
           --tw-bg-opacity: 1;
           background-color: rgb(0 0 0 / var(--tw-bg-opacity));
@@ -195,7 +198,7 @@ crosscheck(({ stable, oxide }) => {
       `)
       oxide.expect(result.css).toMatchFormattedCss(css`
         ${defaults}
-        #app :is(:where(.dark) .dark\:before\:bg-black)::before {
+        #app .dark\:before\:bg-black:where(.dark, .dark *)::before {
           content: var(--tw-content);
           background-color: #000;
         }
diff --git a/tests/kitchen-sink.test.js b/tests/kitchen-sink.test.js
index 44665ffab4a6..27dedb46a8ef 100644
--- a/tests/kitchen-sink.test.js
+++ b/tests/kitchen-sink.test.js
@@ -3,7 +3,7 @@ import { crosscheck, run, html, css, defaults } from './util/run'
 crosscheck(({ stable, oxide }) => {
   test('it works', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
@@ -304,8 +304,10 @@ crosscheck(({ stable, oxide }) => {
           margin-right: auto;
         }
         .drop-empty-rules:hover,
-        .group:hover .apply-group,
-        :is(:where(.dark) .apply-dark-mode) {
+        .group:hover .apply-group {
+          font-weight: 700;
+        }
+        .apply-dark-mode:where(.dark, .dark *) {
           font-weight: 700;
         }
         .apply-with-existing:hover {
@@ -340,7 +342,7 @@ crosscheck(({ stable, oxide }) => {
         .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));
         }
@@ -802,12 +804,12 @@ crosscheck(({ stable, oxide }) => {
             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;
             }
           }
@@ -877,8 +879,10 @@ crosscheck(({ stable, oxide }) => {
           margin-right: auto;
         }
         .drop-empty-rules:hover,
-        .group:hover .apply-group,
-        :is(:where(.dark) .apply-dark-mode) {
+        .group:hover .apply-group {
+          font-weight: 700;
+        }
+        .apply-dark-mode:where(.dark, .dark *) {
           font-weight: 700;
         }
         .apply-with-existing:hover {
@@ -912,7 +916,7 @@ crosscheck(({ stable, oxide }) => {
         .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 *) {
           background-color: #22c55e;
         }
         @media (min-width: 640px) {
@@ -1364,12 +1368,12 @@ crosscheck(({ stable, oxide }) => {
             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 3176b17f3da5..2efbe206a11f 100644
--- a/tests/modify-selectors.test.js
+++ b/tests/modify-selectors.test.js
@@ -5,7 +5,7 @@ import { crosscheck, run, html, css } from './util/run'
 crosscheck(() => {
   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 88b88bd1539a..b5a1a53cfee7 100644
--- a/tests/opacity.test.js
+++ b/tests/opacity.test.js
@@ -3,7 +3,7 @@ import { crosscheck, run, html, css } from './util/run'
 crosscheck(({ stable, oxide }) => {
   test('opacity', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
@@ -43,7 +43,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('colors defined as functions work when opacity plugins are disabled', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
diff --git a/tests/prefix.test.js b/tests/prefix.test.js
index a1fb6733ebc8..a93e570ebbf7 100644
--- a/tests/prefix.test.js
+++ b/tests/prefix.test.js
@@ -5,7 +5,7 @@ crosscheck(({ stable, oxide }) => {
   stable.test('prefix', () => {
     let config = {
       prefix: 'tw-',
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`
@@ -128,7 +128,7 @@ crosscheck(({ stable, oxide }) => {
         .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 {
@@ -171,14 +171,14 @@ crosscheck(({ stable, oxide }) => {
             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/util/apply-important-selector.test.js b/tests/util/apply-important-selector.test.js
index cc2cacf524fd..ddbb86a1c08e 100644
--- a/tests/util/apply-important-selector.test.js
+++ b/tests/util/apply-important-selector.test.js
@@ -3,25 +3,25 @@ import { applyImportantSelector } from '../../src/util/applyImportantSelector'
 
 crosscheck(() => {
   it.each`
-    before                                        | after
-    ${'.foo'}                                     | ${'#app :is(.foo)'}
-    ${'.foo .bar'}                                | ${'#app :is(.foo .bar)'}
-    ${'.foo:hover'}                               | ${'#app :is(.foo:hover)'}
-    ${'.foo .bar:hover'}                          | ${'#app :is(.foo .bar:hover)'}
-    ${'.foo::before'}                             | ${'#app :is(.foo)::before'}
-    ${'.foo::before'}                             | ${'#app :is(.foo)::before'}
-    ${'.foo::file-selector-button'}               | ${'#app :is(.foo)::file-selector-button'}
-    ${'.foo::-webkit-progress-bar'}               | ${'#app :is(.foo)::-webkit-progress-bar'}
-    ${'.foo:hover::before'}                       | ${'#app :is(.foo:hover)::before'}
+    before                                                        | after
+    ${'.foo'}                                                     | ${'#app :is(.foo)'}
+    ${'.foo .bar'}                                                | ${'#app :is(.foo .bar)'}
+    ${'.foo:hover'}                                               | ${'#app :is(.foo:hover)'}
+    ${'.foo .bar:hover'}                                          | ${'#app :is(.foo .bar:hover)'}
+    ${'.foo::before'}                                             | ${'#app :is(.foo)::before'}
+    ${'.foo::before'}                                             | ${'#app :is(.foo)::before'}
+    ${'.foo::file-selector-button'}                               | ${'#app :is(.foo)::file-selector-button'}
+    ${'.foo::-webkit-progress-bar'}                               | ${'#app :is(.foo)::-webkit-progress-bar'}
+    ${'.foo:hover::before'}                                       | ${'#app :is(.foo:hover)::before'}
     ${':is(:where(.dark) :is(:where([dir="rtl"]) .foo::before))'} | ${'#app :is(:where(.dark) :is(:where([dir="rtl"]) .foo))::before'}
-    ${':is(:where(.dark) .foo) .bar'}                     | ${'#app :is(:is(:where(.dark) .foo) .bar)'}
-    ${':is(.foo) :is(.bar)'}                      | ${'#app :is(:is(.foo) :is(.bar))'}
-    ${':is(.foo)::before'}                        | ${'#app :is(.foo)::before'}
-    ${'.foo:before'}                              | ${'#app :is(.foo):before'}
-    ${'.foo::some-uknown-pseudo'}                 | ${'#app :is(.foo)::some-uknown-pseudo'}
-    ${'.foo::some-uknown-pseudo:hover'}           | ${'#app :is(.foo)::some-uknown-pseudo:hover'}
-    ${'.foo:focus::some-uknown-pseudo:hover'}     | ${'#app :is(.foo:focus)::some-uknown-pseudo:hover'}
-    ${'.foo:hover::some-uknown-pseudo:focus'}     | ${'#app :is(.foo:hover)::some-uknown-pseudo:focus'}
+    ${':is(:where(.dark) .foo) .bar'}                             | ${'#app :is(:is(:where(.dark) .foo) .bar)'}
+    ${':is(.foo) :is(.bar)'}                                      | ${'#app :is(:is(.foo) :is(.bar))'}
+    ${':is(.foo)::before'}                                        | ${'#app :is(.foo)::before'}
+    ${'.foo:before'}                                              | ${'#app :is(.foo):before'}
+    ${'.foo::some-uknown-pseudo'}                                 | ${'#app :is(.foo)::some-uknown-pseudo'}
+    ${'.foo::some-uknown-pseudo:hover'}                           | ${'#app :is(.foo)::some-uknown-pseudo:hover'}
+    ${'.foo:focus::some-uknown-pseudo:hover'}                     | ${'#app :is(.foo:focus)::some-uknown-pseudo:hover'}
+    ${'.foo:hover::some-uknown-pseudo:focus'}                     | ${'#app :is(.foo:hover)::some-uknown-pseudo:focus'}
   `('should generate "$after" from "$before"', ({ before, after }) => {
     expect(applyImportantSelector(before, '#app')).toEqual(after)
   })
diff --git a/tests/variants.oxide.test.css b/tests/variants.oxide.test.css
index 2aaab5e70730..c04721895c07 100644
--- a/tests/variants.oxide.test.css
+++ b/tests/variants.oxide.test.css
@@ -319,11 +319,6 @@
     background-color: #fde047;
   }
 }
-@media print {
-  .print\:bg-yellow-300 {
-    background-color: #fde047;
-  }
-}
 @media (min-width: 640px) {
   .sm\:shadow-md,
   .sm\:active\:shadow-md:active {
@@ -389,26 +384,38 @@
     background-color: #fde047;
   }
 }
-:is(:where([dir="ltr"]) .ltr\:shadow-md),
-:is(:where([dir="rtl"]) .rtl\:shadow-md),
-:is(:where(.dark) .dark\:shadow-md),
-:is(
-    :where(.dark)
-      .group:disabled:focus:hover
-      .dark\:group-disabled\:group-focus\:group-hover\:shadow-md
-  ),
-:is(
-    :where(.dark)
-      .peer:disabled:focus:hover
-      ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md
-  ) {
+.ltr\:shadow-md:where([dir="ltr"], [dir="ltr"] *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.rtl\:shadow-md:where([dir="rtl"], [dir="rtl"] *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.dark\:shadow-md:where(.dark, .dark *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.group:disabled:focus:hover .dark\:group-disabled\:group-focus\:group-hover\:shadow-md:where(.dark, .dark *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.peer:disabled:focus:hover ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md:where(.dark, .dark *) {
   --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
   --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
     var(--tw-shadow);
 }
 @media (min-width: 1024px) {
-  :is(:where(.dark) .lg\:dark\:shadow-md) {
+  .lg\:dark\:shadow-md:where(.dark, .dark *) {
     --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
     --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
       0 2px 4px -2px var(--tw-shadow-color);
@@ -417,7 +424,7 @@
   }
 }
 @media (min-width: 1280px) {
-  :is(:where(.dark) .xl\:dark\:disabled\:shadow-md:disabled) {
+  .xl\:dark\:disabled\:shadow-md:disabled:where(.dark, .dark *) {
     --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
     --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
       0 2px 4px -2px var(--tw-shadow-color);
@@ -427,7 +434,7 @@
 }
 @media (min-width: 1536px) {
   @media (prefers-reduced-motion: no-preference) {
-    :is(:where(.dark) .\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within) {
+    .\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within:where(.dark, .dark *) {
       --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
       --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
         0 2px 4px -2px var(--tw-shadow-color);
@@ -441,3 +448,8 @@
     display: flex;
   }
 }
+@media print {
+  .print\:bg-yellow-300 {
+    background-color: #fde047;
+  }
+}
diff --git a/tests/variants.test.css b/tests/variants.test.css
index c2fadabcbab7..95404e1cbe39 100644
--- a/tests/variants.test.css
+++ b/tests/variants.test.css
@@ -337,12 +337,6 @@
     background-color: rgb(253 224 71 / var(--tw-bg-opacity));
   }
 }
-@media print {
-  .print\:bg-yellow-300 {
-    --tw-bg-opacity: 1;
-    background-color: rgb(253 224 71 / var(--tw-bg-opacity));
-  }
-}
 @media (min-width: 640px) {
   .sm\:shadow-md,
   .sm\:active\:shadow-md:active {
@@ -410,26 +404,38 @@
     background-color: rgb(253 224 71 / var(--tw-bg-opacity));
   }
 }
-:is(:where([dir="ltr"]) .ltr\:shadow-md),
-:is(:where([dir="rtl"]) .rtl\:shadow-md),
-:is(:where(.dark) .dark\:shadow-md),
-:is(
-    :where(.dark)
-      .group:disabled:focus:hover
-      .dark\:group-disabled\:group-focus\:group-hover\:shadow-md
-  ),
-:is(
-    :where(.dark)
-      .peer:disabled:focus:hover
-      ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md
-  ) {
+.ltr\:shadow-md:where([dir="ltr"], [dir="ltr"] *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.rtl\:shadow-md:where([dir="rtl"], [dir="rtl"] *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.dark\:shadow-md:where(.dark, .dark *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.group:disabled:focus:hover .dark\:group-disabled\:group-focus\:group-hover\:shadow-md:where(.dark, .dark *) {
+  --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
+  --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+    var(--tw-shadow);
+}
+.peer:disabled:focus:hover ~ .dark\:peer-disabled\:peer-focus\:peer-hover\:shadow-md:where(.dark, .dark *) {
   --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
   --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
     var(--tw-shadow);
 }
 @media (min-width: 1024px) {
-  :is(:where(.dark) .lg\:dark\:shadow-md) {
+  .lg\:dark\:shadow-md:where(.dark, .dark *) {
     --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
     --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
       0 2px 4px -2px var(--tw-shadow-color);
@@ -438,7 +444,7 @@
   }
 }
 @media (min-width: 1280px) {
-  :is(:where(.dark) .xl\:dark\:disabled\:shadow-md:disabled) {
+  .xl\:dark\:disabled\:shadow-md:disabled:where(.dark, .dark *) {
     --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
     --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
       0 2px 4px -2px var(--tw-shadow-color);
@@ -448,7 +454,7 @@
 }
 @media (min-width: 1536px) {
   @media (prefers-reduced-motion: no-preference) {
-    :is(:where(.dark) .\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within) {
+    .\32 xl\:dark\:motion-safe\:focus-within\:shadow-md:focus-within:where(.dark, .dark *) {
       --tw-shadow: 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;
       --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color),
         0 2px 4px -2px var(--tw-shadow-color);
@@ -461,4 +467,10 @@
   .forced-colors\:flex {
     display: flex;
   }
-}
\ No newline at end of file
+}
+@media print {
+  .print\:bg-yellow-300 {
+    --tw-bg-opacity: 1;
+    background-color: rgb(253 224 71 / var(--tw-bg-opacity));
+  }
+}
diff --git a/tests/variants.test.js b/tests/variants.test.js
index 14535dfd0ce1..6cf246476acf 100644
--- a/tests/variants.test.js
+++ b/tests/variants.test.js
@@ -6,7 +6,7 @@ import { crosscheck, run, html, css, defaults } from './util/run'
 crosscheck(({ stable, oxide }) => {
   test('variants', () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [path.resolve(__dirname, './variants.test.html')],
       corePlugins: { preflight: false },
     }
@@ -1156,7 +1156,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('stacking dark and rtl variants', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`<div class="dark:rtl:italic" />`,
@@ -1172,7 +1172,7 @@ crosscheck(({ stable, oxide }) => {
     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;
       }
     `)
@@ -1180,7 +1180,7 @@ crosscheck(({ stable, oxide }) => {
 
   test('stacking dark and rtl variants with pseudo elements', async () => {
     let config = {
-      darkMode: 'class',
+      darkMode: 'selector',
       content: [
         {
           raw: html`<div class="dark:rtl:placeholder:italic" />`,
@@ -1196,7 +1196,10 @@ crosscheck(({ stable, oxide }) => {
     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 b5d9ddc802b8..80b58d07028a 100644
--- a/types/config.d.ts
+++ b/types/config.d.ts
@@ -74,6 +74,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<string, string | Screen | Screen[]>