Repository: tailwindlabs/tailwindcss
Pull request #9546 (2022-10-13 17:40:52)

Upgrade postcss-nested: 5.0.6 → 6.0.0 (major)





Here is everything you need to know about this upgrade. Please take a good look at what changed and the test results before merging this pull request.

### What changed?

#### ✳️ postcss-nested (5.0.6 → 6.0.0) · [Repo](https://github.com/postcss/postcss-nested) · [Changelog](https://github.com/postcss/postcss-nested/blob/main/CHANGELOG.md)



<details>
<summary>Commits</summary>
<p><a href="https://github.com/postcss/postcss-nested/compare/a69ec0afc3073f9fdf06130b6192da18bd8eb69c...bed7d2664bd97d9384e8f9cdb2f7dc85eba8142c">See the full diff on Github</a>. The new version differs by 16 commits:</p>
<ul>
<li><a href="https://github.com/postcss/postcss-nested/commit/bed7d2664bd97d9384e8f9cdb2f7dc85eba8142c"><code>Release 6.0 version</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/6c37bc9f9c5084e29ac4924a8d3c650bd527fd9f"><code>CLean up code from types</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/cb93b652b9cff4befe03e9ffebb3c85652cdd9fb"><code>Update dependencies</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/62a5b852c35b916fbed6bee0796f25a10485b77e"><code>Improve `@at-root` behavior (#147)</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/2e52adaef88e160b46182158c8c2270a2dea1366"><code>Update dependencies</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/0324ebde4896de9ed4ecc2df61a4a0342a04eb77"><code>Fix Node.js 12 support on CI</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/c1cdb0a8e1edc25867a3d79a4096cb2a80222745"><code>Update CI config</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/551e4c82995d75bbcbff06d06a8a574988a10feb"><code>Update CI and dependencies</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/56027681b3e8d2c7653026766cf6f76fc0bfad67"><code>Update dependencies and clean npm ignore</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/58427cc4b0427e489667575149b0e3b29a260d5b"><code>update link to postcss-nesting (#142)</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/3ba3b116bed9c5416b4cfd62de092b154cd21436"><code>Clean up pnpm ignore</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/e61cdb9fe4b32994eee50e4ae54b4b6f441c2614"><code>Move from Jest to uvu</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/28289de11339042d52ba500673db7f27b2c9e09d"><code>Move from yarn to pnpm</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/a5ba59dde5e0b5d730d5afb87bd4157ea0779bd8"><code>Update dependencies</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/8f658006d4fe0249ae24db6500438866bfa4f9b2"><code>Add Node.js 17 to CI</code></a></li>
<li><a href="https://github.com/postcss/postcss-nested/commit/ae9529afb71efdf826922680ee8f74ad51db890a"><code>Bump tmpl from 1.0.4 to 1.0.5 (#138)</code></a></li>
</ul>
</details>






---
![Depfu Status](https://depfu.com/badges/edd6acd35d74c8d41cbb540c30442adf/stats.svg)

[Depfu](https://depfu.com) will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with `@depfu rebase`.

<details><summary>All Depfu comment commands</summary>
<blockquote><dl>
<dt>@​depfu rebase</dt><dd>Rebases against your default branch and redoes this update</dd>
<dt>@​depfu recreate</dt><dd>Recreates this PR, overwriting any edits that you've made to it</dd>
<dt>@​depfu merge</dt><dd>Merges this PR once your tests are passing and conflicts are resolved</dd>
<dt>@​depfu close</dt><dd>Closes this PR and deletes the branch</dd>
<dt>@​depfu reopen</dt><dd>Restores the branch and reopens this PR (if it's closed)</dd>
<dt>@​depfu pause</dt><dd>Ignores all future updates for this dependency and closes this PR</dd>
<dt>@​depfu pause [minor|major]</dt><dd>Ignores all future minor/major updates for this dependency and closes this PR</dd>
<dt>@​depfu resume</dt><dd>Future versions of this dependency will create PRs again (leaves this PR as is)</dd>
</dl></blockquote>
</details>




Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f898437..261b3010 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Added 'place-items-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507))
 - Added 'content-baseline' utility ([#9507](https://github.com/tailwindlabs/tailwindcss/pull/9507))
 - Prepare for container queries setup ([#9526](https://github.com/tailwindlabs/tailwindcss/pull/9526))
+- Add support for modifiers to `matchUtilities` ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541))
+- Switch to positional argument + object for modifiers ([#9541](https://github.com/tailwindlabs/tailwindcss/pull/9541))
+- Add new `min` and `max` variants ([#9558](https://github.com/tailwindlabs/tailwindcss/pull/9558))
+- Add aria variants ([#9557](https://github.com/tailwindlabs/tailwindcss/pull/9557))
 
 ### Fixed
 
@@ -48,7 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Ignore unset values (like `null` or `undefined`) when resolving the classList for intellisense ([#9385](https://github.com/tailwindlabs/tailwindcss/pull/9385))
 - Implement fallback plugins when arbitrary values result in css from multiple plugins ([#9376](https://github.com/tailwindlabs/tailwindcss/pull/9376))
 - Improve type checking for formal syntax ([#9349](https://github.com/tailwindlabs/tailwindcss/pull/9349), [#9448](https://github.com/tailwindlabs/tailwindcss/pull/9448))
-- Don't require `content` key in custom plugin configs ([#9502](https://github.com/tailwindlabs/tailwindcss/pull/9502))
+- Don't require `content` key in custom plugin configs ([#9502](https://github.com/tailwindlabs/tailwindcss/pull/9502), [#9545](https://github.com/tailwindlabs/tailwindcss/pull/9545))
 
 ## [3.1.8] - 2022-08-05
 
diff --git a/package-lock.json b/package-lock.json
index d44f5fbb..e610b61c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,7 +26,7 @@
         "postcss-import": "^14.1.0",
         "postcss-js": "^4.0.0",
         "postcss-load-config": "^3.1.4",
-        "postcss-nested": "5.0.6",
+        "postcss-nested": "6.0.0",
         "postcss-selector-parser": "^6.0.10",
         "postcss-value-parser": "^4.2.0",
         "quick-lru": "^5.1.1",
@@ -5943,11 +5943,11 @@
       }
     },
     "node_modules/postcss-nested": {
-      "version": "5.0.6",
-      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz",
-      "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
+      "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
       "dependencies": {
-        "postcss-selector-parser": "^6.0.6"
+        "postcss-selector-parser": "^6.0.10"
       },
       "engines": {
         "node": ">=12.0"
@@ -11302,11 +11302,11 @@
       }
     },
     "postcss-nested": {
-      "version": "5.0.6",
-      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz",
-      "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz",
+      "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==",
       "requires": {
-        "postcss-selector-parser": "^6.0.6"
+        "postcss-selector-parser": "^6.0.10"
       }
     },
     "postcss-normalize-charset": {
diff --git a/package.json b/package.json
index e299492e..a862de72 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "postcss-import": "^14.1.0",
     "postcss-js": "^4.0.0",
     "postcss-load-config": "^3.1.4",
-    "postcss-nested": "5.0.6",
+    "postcss-nested": "6.0.0",
     "postcss-selector-parser": "^6.0.10",
     "postcss-value-parser": "^4.2.0",
     "quick-lru": "^5.1.1",
diff --git a/src/corePlugins.js b/src/corePlugins.js
index bd1e43c1..fe9573d3 100644
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -12,7 +12,12 @@ import isPlainObject from './util/isPlainObject'
 import transformThemeValue from './util/transformThemeValue'
 import { version as tailwindVersion } from '../package.json'
 import log from './util/log'
-import { normalizeScreens } from './util/normalizeScreens'
+import {
+  normalizeScreens,
+  isScreenSortable,
+  compareScreens,
+  toScreen,
+} from './util/normalizeScreens'
 import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue'
 import { removeAlphaVariables } from './util/removeAlphaVariables'
 import { flagEnabled } from './featureFlags'
@@ -144,28 +149,27 @@ export let variantPlugins = {
     }
 
     let variants = {
-      group: ({ modifier }) =>
+      group: (_, { modifier }) =>
         modifier ? [`:merge(.group\\/${modifier})`, ' &'] : [`:merge(.group)`, ' &'],
-      peer: ({ modifier }) =>
+      peer: (_, { modifier }) =>
         modifier ? [`:merge(.peer\\/${modifier})`, ' ~ &'] : [`:merge(.peer)`, ' ~ &'],
     }
 
     for (let [name, fn] of Object.entries(variants)) {
       matchVariant(
         name,
-        (ctx = {}) => {
-          let { modifier, value = '' } = ctx
-          if (modifier) {
+        (value = '', extra) => {
+          if (extra.modifier) {
             log.warn(`modifier-${name}-experimental`, [
               `The ${name} variant modifier feature in Tailwind CSS is currently in preview.`,
               'Preview features are not covered by semver, and may be improved in breaking ways at any time.',
             ])
           }
 
-          let result = normalize(typeof value === 'function' ? value(ctx) : value)
+          let result = normalize(typeof value === 'function' ? value(extra) : value)
           if (!result.includes('&')) result = '&' + result
 
-          let [a, b] = fn({ modifier })
+          let [a, b] = fn('', extra)
           return result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)
         },
         { values: Object.fromEntries(pseudoVariants) }
@@ -221,18 +225,137 @@ export let variantPlugins = {
     addVariant('print', '@media print')
   },
 
-  screenVariants: ({ theme, addVariant }) => {
-    for (let screen of normalizeScreens(theme('screens'))) {
-      let query = buildMediaQuery(screen)
+  screenVariants: ({ theme, addVariant, matchVariant }) => {
+    let rawScreens = theme('screens') ?? {}
+    let areSimpleScreens = Object.values(rawScreens).every((v) => typeof v === 'string')
+    let screens = normalizeScreens(theme('screens'))
 
-      addVariant(screen.name, `@media ${query}`)
+    /** @type {Set<string>} */
+    let unitCache = new Set([])
+
+    /** @param {string} value */
+    function units(value) {
+      return value.match(/(\D+)$/)?.[1] ?? '(none)'
+    }
+
+    /** @param {string} value */
+    function recordUnits(value) {
+      if (value !== undefined) {
+        unitCache.add(units(value))
+      }
+    }
+
+    /** @param {string} value */
+    function canUseUnits(value) {
+      recordUnits(value)
+
+      // If the cache was empty it'll become 1 because we've just added the current unit
+      // If the cache was not empty and the units are the same the size doesn't change
+      // Otherwise, if the units are different from what is already known the size will always be > 1
+      return unitCache.size === 1
+    }
+
+    for (const screen of screens) {
+      for (const value of screen.values) {
+        recordUnits(value.min)
+        recordUnits(value.max)
+      }
+    }
+
+    let screensUseConsistentUnits = unitCache.size <= 1
+
+    /**
+     * @typedef {import('./util/normalizeScreens').Screen} Screen
+     */
+
+    /**
+     * @param {'min' | 'max'} type
+     * @returns {Record<string, Screen>}
+     */
+    function buildScreenValues(type) {
+      return Object.fromEntries(
+        screens
+          .filter((screen) => isScreenSortable(screen).result)
+          .map((screen) => {
+            let { min, max } = screen.values[0]
+
+            if (type === 'min' && min !== undefined) {
+              return screen
+            } else if (type === 'min' && max !== undefined) {
+              return { ...screen, not: !screen.not }
+            } else if (type === 'max' && max !== undefined) {
+              return screen
+            } else if (type === 'max' && min !== undefined) {
+              return { ...screen, not: !screen.not }
+            }
+          })
+          .map((screen) => [screen.name, screen])
+      )
+    }
+
+    /**
+     * @param {'min' | 'max'} type
+     * @returns {(a: { value: string | Screen }, z: { value: string | Screen }) => number}
+     */
+    function buildSort(type) {
+      return (a, z) => compareScreens(type, a.value, z.value)
+    }
+
+    let maxSort = buildSort('max')
+    let minSort = buildSort('min')
+
+    /** @param {'min'|'max'} type */
+    function buildScreenVariant(type) {
+      return (value) => {
+        if (!areSimpleScreens) {
+          log.warn('complex-screen-config', [
+            'The min and max variants are not supported with a screen configuration containing objects.',
+          ])
+
+          return []
+        } else if (!screensUseConsistentUnits) {
+          log.warn('mixed-screen-units', [
+            'The min and max variants are not supported with a screen configuration containing mixed units.',
+          ])
+
+          return []
+        } else if (typeof value === 'string' && !canUseUnits(value)) {
+          log.warn('minmax-have-mixed-units', [
+            'The min and max variants are not supported with a screen configuration containing mixed units.',
+          ])
+
+          return []
+        }
+
+        return [`@media ${buildMediaQuery(toScreen(value, type))}`]
+      }
     }
+
+    matchVariant('max', buildScreenVariant('max'), {
+      sort: maxSort,
+      values: areSimpleScreens ? buildScreenValues('max') : {},
+    })
+
+    // screens and min-* are sorted together when they can be
+    let id = 'min-screens'
+    for (let screen of screens) {
+      addVariant(screen.name, `@media ${buildMediaQuery(screen)}`, {
+        id,
+        sort: areSimpleScreens && screensUseConsistentUnits ? minSort : undefined,
+        value: screen,
+      })
+    }
+
+    matchVariant('min', buildScreenVariant('min'), {
+      id,
+      sort: minSort,
+    })
   },
 
   supportsVariants: ({ matchVariant, theme }) => {
     matchVariant(
       'supports',
-      ({ value = '' }) => {
+      (value = '') => {
         let check = normalize(value)
         let isRaw = /^\w*\s*\(/.test(check)
 
@@ -258,6 +381,26 @@ export let variantPlugins = {
     )
   },
 
+  ariaVariants: ({ matchVariant, theme }) => {
+    matchVariant('aria', (value) => `&[aria-${value}]`, { values: theme('aria') ?? {} })
+    matchVariant(
+      'group-aria',
+      (value, { modifier }) =>
+        modifier
+          ? `:merge(.group\\/${modifier})[aria-${value}] &`
+          : `:merge(.group)[aria-${value}] &`,
+      { values: theme('aria') ?? {} }
+    )
+    matchVariant(
+      'peer-aria',
+      (value, { modifier }) =>
+        modifier
+          ? `:merge(.peer\\/${modifier})[aria-${value}] ~ &`
+          : `:merge(.peer)[aria-${value}] ~ &`,
+      { values: theme('aria') ?? {} }
+    )
+  },
+
   orientationVariants: ({ addVariant }) => {
     addVariant('portrait', '@media (orientation: portrait)')
     addVariant('landscape', '@media (orientation: landscape)')
diff --git a/src/featureFlags.js b/src/featureFlags.js
index 2bfee94d..f07229d4 100644
--- a/src/featureFlags.js
+++ b/src/featureFlags.js
@@ -3,6 +3,7 @@ import log from './util/log'
 
 let defaults = {
   optimizeUniversalDefaults: false,
+  generalizedModifiers: true,
 }
 
 let featureFlags = {
@@ -14,6 +15,7 @@ let featureFlags = {
   ],
   experimental: [
     'optimizeUniversalDefaults',
+    'generalizedModifiers',
     // 'variantGrouping',
   ],
 }
diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js
index 20200490..961b6fa5 100644
--- a/src/lib/generateRules.js
+++ b/src/lib/generateRules.js
@@ -3,7 +3,7 @@ import selectorParser from 'postcss-selector-parser'
 import parseObjectStyles from '../util/parseObjectStyles'
 import isPlainObject from '../util/isPlainObject'
 import prefixSelector from '../util/prefixSelector'
-import { updateAllClasses, typeMap } from '../util/pluginUtils'
+import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
 import log from '../util/log'
 import * as sharedState from './sharedState'
 import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
@@ -34,13 +34,24 @@ function* candidatePermutations(candidate) {
 
   while (lastIndex >= 0) {
     let dashIdx
+    let wasSlash = false
 
     if (lastIndex === Infinity && candidate.endsWith(']')) {
       let bracketIdx = candidate.indexOf('[')
 
       // If character before `[` isn't a dash or a slash, this isn't a dynamic class
       // eg. string[]
-      dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
+      if (candidate[bracketIdx - 1] === '-') {
+        dashIdx = bracketIdx - 1
+      } else if (candidate[bracketIdx - 1] === '/') {
+        dashIdx = bracketIdx - 1
+        wasSlash = true
+      } else {
+        dashIdx = -1
+      }
+    } else if (lastIndex === Infinity && candidate.includes('/')) {
+      dashIdx = candidate.lastIndexOf('/')
+      wasSlash = true
     } else {
       dashIdx = candidate.lastIndexOf('-', lastIndex)
     }
@@ -50,11 +61,16 @@ function* candidatePermutations(candidate) {
     }
 
     let prefix = candidate.slice(0, dashIdx)
-    let modifier = candidate.slice(dashIdx + 1)
-
-    yield [prefix, modifier]
+    let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
 
     lastIndex = dashIdx - 1
+
+    // TODO: This feels a bit hacky
+    if (prefix === '' || modifier === '/') {
+      continue
+    }
+
+    yield [prefix, modifier]
   }
 }
 
@@ -137,6 +153,10 @@ function applyVariant(variant, matches, context) {
     if (match) {
       variant = match[1]
       args.modifier = match[2]
+
+      if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
+        return []
+      }
     }
   }
 
@@ -552,16 +572,14 @@ function* resolveMatches(candidate, context, original = candidate) {
       }
 
       if (matchesPerPlugin.length > 0) {
-        let matchingTypes = (sort.options?.types ?? [])
-          .map(({ type }) => type)
-          // Only track the types for this plugin that resulted in some result
-          .filter((type) => {
-            return Boolean(
-              typeMap[type](modifier, sort.options, {
-                tailwindConfig: context.tailwindConfig,
-              })
-            )
-          })
+        let matchingTypes = Array.from(
+          getMatchingTypes(
+            sort.options?.types ?? [],
+            modifier,
+            sort.options ?? {},
+            context.tailwindConfig
+          )
+        ).map(([_, type]) => type)
 
         if (matchingTypes.length > 0) {
           typesByMatches.set(matchesPerPlugin, matchingTypes)
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index fa0969ff..18650509 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -21,6 +21,7 @@ import isValidArbitraryValue from '../util/isValidArbitraryValue'
 import { generateRules } from './generateRules'
 import { hasContentChanged } from './cacheInvalidation.js'
 import { Offsets } from './offsets.js'
+import { flagEnabled } from '../featureFlags.js'
 
 let MATCH_VARIANT = Symbol()
 
@@ -358,6 +359,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
       let defaultOptions = {
         respectPrefix: true,
         respectImportant: true,
+        modifiers: false,
       }
 
       options = normalizeOptionTypes({ ...defaultOptions, ...options })
@@ -371,7 +373,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
         classList.add([prefixedIdentifier, options])
 
         function wrapped(modifier, { isOnlyPlugin }) {
-          let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)
+          let [value, coercedType, utilityModifier] = coerceValue(
+            options.types,
+            modifier,
+            options,
+            tailwindConfig
+          )
 
           if (value === undefined) {
             return []
@@ -395,8 +402,22 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
             return []
           }
 
+          let extras = {
+            get modifier() {
+              if (!options.modifiers) {
+                log.warn(`modifier-used-without-options-for-${identifier}`, [
+                  'Your plugin must set `modifiers: true` in its options to support modifiers.',
+                ])
+              }
+
+              return utilityModifier
+            },
+          }
+
+          let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
+
           let ruleSets = []
-            .concat(rule(value))
+            .concat(modifiersEnabled ? rule(value, extras) : rule(value))
             .filter(Boolean)
             .map((declaration) => ({
               [nameClass(identifier, modifier)]: declaration,
@@ -418,6 +439,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
       let defaultOptions = {
         respectPrefix: true,
         respectImportant: false,
+        modifiers: false,
       }
 
       options = normalizeOptionTypes({ ...defaultOptions, ...options })
@@ -431,7 +453,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
         classList.add([prefixedIdentifier, options])
 
         function wrapped(modifier, { isOnlyPlugin }) {
-          let [value, coercedType] = coerceValue(options.types, modifier, options, tailwindConfig)
+          let [value, coercedType, utilityModifier] = coerceValue(
+            options.types,
+            modifier,
+            options,
+            tailwindConfig
+          )
 
           if (value === undefined) {
             return []
@@ -455,8 +482,22 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
             return []
           }
 
+          let extras = {
+            get modifier() {
+              if (!options.modifiers) {
+                log.warn(`modifier-used-without-options-for-${identifier}`, [
+                  'Your plugin must set `modifiers: true` in its options to support modifiers.',
+                ])
+              }
+
+              return utilityModifier
+            },
+          }
+
+          let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
+
           let ruleSets = []
-            .concat(rule(value))
+            .concat(modifiersEnabled ? rule(value, extras) : rule(value))
             .filter(Boolean)
             .map((declaration) => ({
               [nameClass(identifier, modifier)]: declaration,
@@ -519,24 +560,42 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
       context.variantOptions.set(variantName, options)
     },
     matchVariant(variant, variantFn, options) {
-      let id = ++variantIdentifier // A unique identifier that "groups" these variables together.
+      // A unique identifier that "groups" these variants together.
+      // This is for internal use only which is why it is not present in the types
+      let id = options?.id ?? ++variantIdentifier
       let isSpecial = variant === '@'
 
+      let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
+
       for (let [key, value] of Object.entries(options?.values ?? {})) {
         api.addVariant(
           isSpecial ? `${variant}${key}` : `${variant}-${key}`,
-          Object.assign(({ args, container }) => variantFn({ ...args, container, value }), {
-            [MATCH_VARIANT]: true,
-          }),
+          Object.assign(
+            ({ args, container }) =>
+              variantFn(
+                value,
+                modifiersEnabled ? { modifier: args.modifier, container } : { container }
+              ),
+            {
+              [MATCH_VARIANT]: true,
+            }
+          ),
           { ...options, value, id }
         )
       }
 
       api.addVariant(
         variant,
-        Object.assign(({ args, container }) => variantFn({ ...args, container }), {
-          [MATCH_VARIANT]: true,
-        }),
+        Object.assign(
+          ({ args, container }) =>
+            variantFn(
+              args.value,
+              modifiersEnabled ? { modifier: args.modifier, container } : { container }
+            ),
+          {
+            [MATCH_VARIANT]: true,
+          }
+        ),
         { ...options, id }
       )
     },
@@ -658,6 +717,7 @@ function resolvePlugins(context, root) {
   let beforeVariants = [
     variantPlugins['pseudoElementVariants'],
     variantPlugins['pseudoClassVariants'],
+    variantPlugins['ariaVariants'],
   ]
   let afterVariants = [
     variantPlugins['supportsVariants'],
diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js
index ccfc63ba..8489dd4b 100644
--- a/src/util/buildMediaQuery.js
+++ b/src/util/buildMediaQuery.js
@@ -2,8 +2,8 @@ export default function buildMediaQuery(screens) {
   screens = Array.isArray(screens) ? screens : [screens]
 
   return screens
-    .map((screen) =>
-      screen.values.map((screen) => {
+    .map((screen) => {
+      let values = screen.values.map((screen) => {
         if (screen.raw !== undefined) {
           return screen.raw
         }
@@ -15,6 +15,8 @@ export default function buildMediaQuery(screens) {
           .filter(Boolean)
           .join(' and ')
       })
-    )
+
+      return screen.not ? `not all and ${values}` : values
+    })
     .join(', ')
 }
diff --git a/src/util/nameClass.js b/src/util/nameClass.js
index ae737012..cb37dba2 100644
--- a/src/util/nameClass.js
+++ b/src/util/nameClass.js
@@ -22,5 +22,9 @@ export function formatClass(classPrefix, key) {
     return `-${classPrefix}${key}`
   }
 
+  if (key.startsWith('/')) {
+    return `${classPrefix}${key}`
+  }
+
   return `${classPrefix}-${key}`
 }
diff --git a/src/util/normalizeScreens.js b/src/util/normalizeScreens.js
index 8f2ff709..559f7ccb 100644
--- a/src/util/normalizeScreens.js
+++ b/src/util/normalizeScreens.js
@@ -1,3 +1,17 @@
+/**
+ * @typedef {object} ScreenValue
+ * @property {number|undefined} min
+ * @property {number|undefined} max
+ * @property {string|undefined} raw
+ */
+
+/**
+ * @typedef {object} Screen
+ * @property {string} name
+ * @property {boolean} not
+ * @property {ScreenValue[]} values
+ */
+
 /**
  * A function that normalizes the various forms that the screens object can be
  * provided in.
@@ -10,6 +24,8 @@
  *
  * Output(s):
  *   - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
+ *
+ * @returns {Screen[]}
  */
 export function normalizeScreens(screens, root = true) {
   if (Array.isArray(screens)) {
@@ -19,27 +35,106 @@ export function normalizeScreens(screens, root = true) {
       }
 
       if (typeof screen === 'string') {
-        return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
+        return { name: screen.toString(), not: false, values: [{ min: screen, max: undefined }] }
       }
 
       let [name, options] = screen
       name = name.toString()
 
       if (typeof options === 'string') {
-        return { name, values: [{ min: options, max: undefined }] }
+        return { name, not: false, values: [{ min: options, max: undefined }] }
       }
 
       if (Array.isArray(options)) {
-        return { name, values: options.map((option) => resolveValue(option)) }
+        return { name, not: false, values: options.map((option) => resolveValue(option)) }
       }
 
-      return { name, values: [resolveValue(options)] }
+      return { name, not: false, values: [resolveValue(options)] }
     })
   }
 
   return normalizeScreens(Object.entries(screens ?? {}), false)
 }
 
+/**
+ * @param {Screen} screen
+ * @returns {{result: false, reason: string} | {result: true, reason: null}}
+ */
+export function isScreenSortable(screen) {
+  if (screen.values.length !== 1) {
+    return { result: false, reason: 'multiple-values' }
+  } else if (screen.values[0].raw !== undefined) {
+    return { result: false, reason: 'raw-values' }
+  } else if (screen.values[0].min !== undefined && screen.values[0].max !== undefined) {
+    return { result: false, reason: 'min-and-max' }
+  }
+
+  return { result: true, reason: null }
+}
+
+/**
+ * @param {'min' | 'max'} type
+ * @param {Screen | 'string'} a
+ * @param {Screen | 'string'} z
+ * @returns {number}
+ */
+export function compareScreens(type, a, z) {
+  let aScreen = toScreen(a, type)
+  let zScreen = toScreen(z, type)
+
+  let aSorting = isScreenSortable(aScreen)
+  let bSorting = isScreenSortable(zScreen)
+
+  // These cases should never happen and indicate a bug in Tailwind CSS itself
+  if (aSorting.reason === 'multiple-values' || bSorting.reason === 'multiple-values') {
+    throw new Error(
+      'Attempted to sort a screen with multiple values. This should never happen. Please open a bug report.'
+    )
+  } else if (aSorting.reason === 'raw-values' || bSorting.reason === 'raw-values') {
+    throw new Error(
+      'Attempted to sort a screen with raw values. This should never happen. Please open a bug report.'
+    )
+  } else if (aSorting.reason === 'min-and-max' || bSorting.reason === 'min-and-max') {
+    throw new Error(
+      'Attempted to sort a screen with both min and max values. This should never happen. Please open a bug report.'
+    )
+  }
+
+  // Let the sorting begin
+  let { min: aMin, max: aMax } = aScreen.values[0]
+  let { min: zMin, max: zMax } = zScreen.values[0]
+
+  // Negating screens flip their behavior. Basically `not min-width` is `max-width`
+  if (a.not) [aMin, aMax] = [aMax, aMin]
+  if (z.not) [zMin, zMax] = [zMax, zMin]
+
+  aMin = aMin === undefined ? aMin : parseFloat(aMin)
+  aMax = aMax === undefined ? aMax : parseFloat(aMax)
+  zMin = zMin === undefined ? zMin : parseFloat(zMin)
+  zMax = zMax === undefined ? zMax : parseFloat(zMax)
+
+  let [aValue, zValue] = type === 'min' ? [aMin, zMin] : [zMax, aMax]
+
+  return aValue - zValue
+}
+
+/**
+ *
+ * @param {PartialScreen> | string} value
+ * @param {'min' | 'max'} type
+ * @returns {Screen}
+ */
+export function toScreen(value, type) {
+  if (typeof value === 'object') {
+    return value
+  }
+
+  return {
+    name: 'arbitrary-screen',
+    values: [{ [type]: value }],
+  }
+}
+
 function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
   return { min, max, raw }
 }
diff --git a/src/util/pluginUtils.js b/src/util/pluginUtils.js
index c56f6a1d..d5ad3fa3 100644
--- a/src/util/pluginUtils.js
+++ b/src/util/pluginUtils.js
@@ -19,6 +19,7 @@ import {
 } from './dataTypes'
 import negateValue from './negateValue'
 import { backgroundSize } from './validateFormalSyntax'
+import { flagEnabled } from '../featureFlags.js'
 
 export function updateAllClasses(selectors, updateClass) {
   let parser = selectorParser((selectors) => {
@@ -86,11 +87,20 @@ function isArbitraryValue(input) {
   return input.startsWith('[') && input.endsWith(']')
 }
 
-function splitAlpha(modifier) {
+function splitUtilityModifier(modifier) {
   let slashIdx = modifier.lastIndexOf('/')
 
   if (slashIdx === -1 || slashIdx === modifier.length - 1) {
-    return [modifier]
+    return [modifier, undefined]
+  }
+
+  let arbitrary = isArbitraryValue(modifier)
+
+  // The modifier could be of the form `[foo]/[bar]`
+  // We want to handle this case properly
+  // without affecting `[foo/bar]`
+  if (arbitrary && !modifier.includes(']/[')) {
+    return [modifier, undefined]
   }
 
   return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
@@ -106,12 +116,18 @@ export function parseColorFormat(value) {
   return value
 }
 
-export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
-  if (options.values?.[modifier] !== undefined) {
-    return parseColorFormat(options.values?.[modifier])
+export function asColor(
+  _,
+  options = {},
+  { tailwindConfig = {}, utilityModifier, rawModifier } = {}
+) {
+  if (options.values?.[rawModifier] !== undefined) {
+    return parseColorFormat(options.values?.[rawModifier])
   }
 
-  let [color, alpha] = splitAlpha(modifier)
+  // TODO: Hoist this up to getMatchingTypes or something
+  // We do this here because we need the alpha value (if any)
+  let [color, alpha] = splitUtilityModifier(rawModifier)
 
   if (alpha !== undefined) {
     let normalizedColor =
@@ -134,7 +150,7 @@ export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
     return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
   }
 
-  return asValue(modifier, options, { validate: validateColor })
+  return asValue(rawModifier, options, { rawModifier, utilityModifier, validate: validateColor })
 }
 
 export function asLookupValue(modifier, options = {}) {
@@ -142,8 +158,8 @@ export function asLookupValue(modifier, options = {}) {
 }
 
 function guess(validate) {
-  return (modifier, options) => {
-    return asValue(modifier, options, { validate })
+  return (modifier, options, extras) => {
+    return asValue(modifier, options, { ...extras, validate })
   }
 }
 
@@ -192,15 +208,73 @@ export function coerceValue(types, modifier, options, tailwindConfig) {
     }
 
     if (value.length > 0 && supportedTypes.includes(explicitType)) {
-      return [asValue(`[${value}]`, options), explicitType]
+      return [asValue(`[${value}]`, options), explicitType, null]
     }
   }
 
+  let matches = getMatchingTypes(types, modifier, options, tailwindConfig)
+
   // Find first matching type
-  for (let { type } of types) {
-    let result = typeMap[type](modifier, options, { tailwindConfig })
-    if (result !== undefined) return [result, type]
+  for (let match of matches) {
+    return match
   }
 
   return []
 }
+
+/**
+ *
+ * @param {{type: string}[]} types
+ * @param {string} rawModifier
+ * @param {any} options
+ * @param {any} tailwindConfig
+ * @returns {Iterator<[value: string, type: string, modifier: string | null]>}
+ */
+export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
+  let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
+
+  let canUseUtilityModifier =
+    modifiersEnabled &&
+    options.modifiers != null &&
+    (options.modifiers === 'any' || typeof options.modifiers === 'object')
+
+  let [modifier, utilityModifier] = canUseUtilityModifier
+    ? splitUtilityModifier(rawModifier)
+    : [rawModifier, undefined]
+
+  if (utilityModifier !== undefined && modifier === '') {
+    modifier = 'DEFAULT'
+  }
+
+  // Check the full value first
+  // TODO: Move to asValue… somehow
+  if (utilityModifier !== undefined) {
+    if (typeof options.modifiers === 'object') {
+      let configValue = options.modifiers?.[utilityModifier] ?? null
+      if (configValue !== null) {
+        utilityModifier = configValue
+      } else if (isArbitraryValue(utilityModifier)) {
+        utilityModifier = utilityModifier.slice(1, -1)
+      }
+    }
+
+    let result = asValue(rawModifier, options, { rawModifier, utilityModifier, tailwindConfig })
+    if (result !== undefined) {
+      yield [result, 'any', null]
+    }
+  }
+
+  for (const { type } of types ?? []) {
+    let result = typeMap[type](modifier, options, {
+      rawModifier,
+      utilityModifier,
+      tailwindConfig,
+    })
+
+    if (result === undefined) {
+      continue
+    }
+
+    yield [result, type, utilityModifier ?? null]
+  }
+}
diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js
index 49750a2a..0d0ec6a7 100644
--- a/stubs/defaultConfig.stub.js
+++ b/stubs/defaultConfig.stub.js
@@ -112,6 +112,16 @@ module.exports = {
       pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
       bounce: 'bounce 1s infinite',
     },
+    aria: {
+      checked: 'checked="true"',
+      disabled: 'disabled="true"',
+      expanded: 'expanded="true"',
+      hidden: 'hidden="true"',
+      pressed: 'pressed="true"',
+      readonly: 'readonly="true"',
+      required: 'required="true"',
+      selected: 'selected="true"',
+    },
     aspectRatio: {
       auto: 'auto',
       square: '1 / 1',
diff --git a/types/config.d.ts b/types/config.d.ts
index 4ea61e7d..0df36206 100644
--- a/types/config.d.ts
+++ b/types/config.d.ts
@@ -12,7 +12,7 @@ interface RecursiveKeyValuePair<K extends keyof any = string, V = string> {
   [key: string]: V | RecursiveKeyValuePair<K, V>
 }
 type ResolvableTo<T> = T | ((utils: PluginUtils) => T)
-type CSSRuleObject = RecursiveKeyValuePair<string, string | string[]>
+type CSSRuleObject = RecursiveKeyValuePair<string, null | string | string[]>
 
 interface PluginUtils {
   colors: DefaultColors
@@ -263,13 +263,17 @@ export interface PluginAPI {
     }>
   ): void
   // for registering new dynamic utility styles
-  matchUtilities<T>(
-    utilities: KeyValuePair<string, (value: T) => CSSRuleObject>,
+  matchUtilities<T = string, U = string>(
+    utilities: KeyValuePair<
+      string,
+      (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject | null
+    >,
     options?: Partial<{
       respectPrefix: boolean
       respectImportant: boolean
       type: ValueType | ValueType[]
       values: KeyValuePair<string, T>
+      modifiers: 'any' | KeyValuePair<string, U>
       supportsNegativeValues: boolean
     }>
   ): void
@@ -282,13 +286,17 @@ export interface PluginAPI {
     }>
   ): void
   // for registering new dynamic component styles
-  matchComponents<T>(
-    components: KeyValuePair<string, (value: T) => CSSRuleObject>,
+  matchComponents<T = string, U = string>(
+    components: KeyValuePair<
+      string,
+      (value: T | string, extra: { modifier: U | string | null }) => CSSRuleObject | null
+    >,
     options?: Partial<{
       respectPrefix: boolean
       respectImportant: boolean
       type: ValueType | ValueType[]
       values: KeyValuePair<string, T>
+      modifiers: 'any' | KeyValuePair<string, U>
       supportsNegativeValues: boolean
     }>
   ): void
@@ -296,18 +304,14 @@ export interface PluginAPI {
   addBase(base: CSSRuleObject | CSSRuleObject[]): void
   // for registering custom variants
   addVariant(name: string, definition: string | string[] | (() => string) | (() => string)[]): void
-  matchVariant(
-    name: string,
-    cb: (options: { value: string; modifier: string | null }) => string | string[]
-  ): void
-  matchVariant<Values extends {}>(
+  matchVariant<T = string>(
     name: string,
-    cb: (options: { value: string; modifier: string | null }) => string | string[],
-    options: {
-      values: Values
-      sort(
-        a: { value: keyof Values | string; modifier: string | null },
-        b: { value: keyof Values | string; modifier: string | null }
+    cb: (value: T | string, extra: { modifier: string | null }) => string | string[],
+    options?: {
+      values?: KeyValuePair<string, T>
+      sort?(
+        a: { value: T | string; modifier: string | null },
+        b: { value: T | string; modifier: string | null }
       ): number
     }
   ): void
@@ -326,8 +330,11 @@ export interface PluginAPI {
 export type PluginCreator = (api: PluginAPI) => void
 export type PluginsConfig = (
   | PluginCreator
-  | { handler: PluginCreator; config?: Config }
-  | { (options: any): { handler: PluginCreator; config?: Config }; __isOptionsFunction: true }
+  | { handler: PluginCreator; config?: Partial<Config> }
+  | {
+      (options: any): { handler: PluginCreator; config?: Partial<Config> }
+      __isOptionsFunction: true
+    }
 )[]
 
 // Top level config related


Test patch:
diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js
index d259971f..91b57f5b 100644
--- a/tests/arbitrary-variants.test.js
+++ b/tests/arbitrary-variants.test.js
@@ -616,6 +616,69 @@ test('classes in the same arbitrary variant should not be prefixed', () => {
   })
 })
 
+it('should support aria variants', () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div>
+            <div class="aria-checked:underline"></div>
+            <div class="aria-[sort=ascending]:underline"></div>
+            <div class="group-aria-checked:underline"></div>
+            <div class="peer-aria-checked:underline"></div>
+            <div class="group-aria-checked/foo:underline"></div>
+            <div class="peer-aria-checked/foo:underline"></div>
+            <div class="group-aria-[sort=ascending]:underline"></div>
+            <div class="peer-aria-[sort=ascending]:underline"></div>
+            <div class="group-aria-[sort=ascending]/foo:underline"></div>
+            <div class="peer-aria-[sort=ascending]/foo:underline"></div>
+          </div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .aria-checked\:underline[aria-checked='true'] {
+        text-decoration-line: underline;
+      }
+      .aria-\[sort\=ascending\]\:underline[aria-sort='ascending'] {
+        text-decoration-line: underline;
+      }
+      .group[aria-checked='true'] .group-aria-checked\:underline {
+        text-decoration-line: underline;
+      }
+      .group\/foo[aria-checked='true'] .group-aria-checked\/foo\:underline {
+        text-decoration-line: underline;
+      }
+      .group[aria-sort='ascending'] .group-aria-\[sort\=ascending\]\:underline {
+        text-decoration-line: underline;
+      }
+      .group\/foo[aria-sort='ascending'] .group-aria-\[sort\=ascending\]\/foo\:underline {
+        text-decoration-line: underline;
+      }
+      .peer[aria-checked='true'] ~ .peer-aria-checked\:underline {
+        text-decoration-line: underline;
+      }
+      .peer\/foo[aria-checked='true'] ~ .peer-aria-checked\/foo\:underline {
+        text-decoration-line: underline;
+      }
+      .peer[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\:underline {
+        text-decoration-line: underline;
+      }
+      .peer\/foo[aria-sort='ascending'] ~ .peer-aria-\[sort\=ascending\]\/foo\:underline {
+        text-decoration-line: underline;
+      }
+    `)
+  })
+})
+
 it('should support supports', () => {
   let config = {
     theme: {
diff --git a/tests/match-utilities.test.js b/tests/match-utilities.test.js
new file mode 100644
index 00000000..587a4686
--- /dev/null
+++ b/tests/match-utilities.test.js
@@ -0,0 +1,167 @@
+import { run, html, css } from './util/run'
+
+test('match utilities with modifiers', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="test test/foo test-1/foo test-2/foo test/[foo] test-1/[foo]"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+
+    plugins: [
+      ({ matchUtilities }) => {
+        matchUtilities(
+          {
+            test: (value, { modifier }) => ({
+              color: `${value}_${modifier}`,
+            }),
+          },
+          {
+            values: {
+              DEFAULT: 'default',
+              bar: 'bar',
+              '1': 'one',
+              '2': 'two',
+              '1/foo': 'onefoo',
+            },
+            modifiers: 'any',
+          }
+        )
+      },
+    ],
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .test {
+      color: default_null;
+    }
+    .test\/foo {
+      color: default_foo;
+    }
+    .test-1\/foo {
+      color: onefoo_null;
+    }
+    .test-2\/foo {
+      color: two_foo;
+    }
+    .test\/\[foo\] {
+      color: default_[foo];
+    }
+    .test-1\/\[foo\] {
+      color: one_[foo];
+    }
+  `)
+})
+
+test('match utilities with modifiers in the config', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="test test/foo test-1/foo test/[bar] test-1/[bar]"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+
+    plugins: [
+      ({ matchUtilities }) => {
+        matchUtilities(
+          {
+            test: (value, { modifier }) => ({
+              color: `${value}_${modifier}`,
+            }),
+          },
+          {
+            values: {
+              DEFAULT: 'default',
+              bar: 'bar',
+              '1': 'one',
+            },
+            modifiers: {
+              foo: 'mewtwo',
+            },
+          }
+        )
+      },
+    ],
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .test {
+      color: default_null;
+    }
+    .test\/foo {
+      color: default_mewtwo;
+    }
+    .test-1\/foo {
+      color: one_mewtwo;
+    }
+    .test\/\[bar\] {
+      color: default_bar;
+    }
+    .test-1\/\[bar\] {
+      color: one_bar;
+    }
+  `)
+})
+
+test('match utilities can omit utilities by returning null', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="test test/good test/bad"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+
+    plugins: [
+      ({ matchUtilities }) => {
+        matchUtilities(
+          {
+            test: (value, { modifier }) =>
+              modifier === 'bad'
+                ? null
+                : {
+                    color: `${value}_${modifier}`,
+                  },
+          },
+          {
+            values: {
+              DEFAULT: 'default',
+              bar: 'bar',
+              '1': 'one',
+            },
+            modifiers: 'any',
+          }
+        )
+      },
+    ],
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .test {
+      color: default_null;
+    }
+    .test\/good {
+      color: default_good;
+    }
+  `)
+})
diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js
index 7b80a063..7da1b423 100644
--- a/tests/match-variants.test.js
+++ b/tests/match-variants.test.js
@@ -10,7 +10,7 @@ test('partial arbitrary variants', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('potato', ({ value: flavor }) => `.potato-${flavor} &`)
+        matchVariant('potato', (flavor) => `.potato-${flavor} &`)
       },
     ],
   }
@@ -43,7 +43,7 @@ test('partial arbitrary variants with at-rules', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('potato', ({ value: flavor }) => `@media (potato: ${flavor})`)
+        matchVariant('potato', (flavor) => `@media (potato: ${flavor})`)
       },
     ],
   }
@@ -79,7 +79,7 @@ test('partial arbitrary variants with at-rules and placeholder', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('potato', ({ value: flavor }) => `@media (potato: ${flavor}) { &:potato }`)
+        matchVariant('potato', (flavor) => `@media (potato: ${flavor}) { &:potato }`)
       },
     ],
   }
@@ -115,7 +115,7 @@ test('partial arbitrary variants with default values', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('tooltip', ({ value: side }) => `&${side}`, {
+        matchVariant('tooltip', (side) => `&${side}`, {
           values: {
             bottom: '[data-location="bottom"]',
             top: '[data-location="top"]',
@@ -154,7 +154,7 @@ test('matched variant values maintain the sort order they are registered in', ()
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('alphabet', ({ value: side }) => `&${side}`, {
+        matchVariant('alphabet', (side) => `&${side}`, {
           values: {
             a: '[data-value="a"]',
             b: '[data-value="b"]',
@@ -201,7 +201,7 @@ test('matchVariant can return an array of format strings from the function', ()
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('test', ({ value: selector }) =>
+        matchVariant('test', (selector) =>
           selector.split(',').map((selector) => `&.${selector} > *`)
         )
       },
@@ -235,7 +235,7 @@ it('should be possible to sort variants', () => {
       {
         raw: html`
           <div>
-            <div class="min-[500px]:underline min-[700px]:italic"></div>
+            <div class="testmin-[500px]:underline testmin-[700px]:italic"></div>
           </div>
         `,
       },
@@ -243,7 +243,7 @@ it('should be possible to sort variants', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           sort(a, z) {
             return parseInt(a.value) - parseInt(z.value)
           },
@@ -259,13 +259,13 @@ it('should be possible to sort variants', () => {
   return run(input, config).then((result) => {
     expect(result.css).toMatchFormattedCss(css`
       @media (min-width: 500px) {
-        .min-\[500px\]\:underline {
+        .testmin-\[500px\]\:underline {
           text-decoration-line: underline;
         }
       }
 
       @media (min-width: 700px) {
-        .min-\[700px\]\:italic {
+        .testmin-\[700px\]\:italic {
           font-style: italic;
         }
       }
@@ -279,7 +279,7 @@ it('should be possible to compare arbitrary variants and hardcoded variants', ()
       {
         raw: html`
           <div>
-            <div class="min-[700px]:italic min-example:italic min-[500px]:italic"></div>
+            <div class="testmin-[700px]:italic testmin-example:italic testmin-[500px]:italic"></div>
           </div>
         `,
       },
@@ -287,7 +287,7 @@ it('should be possible to compare arbitrary variants and hardcoded variants', ()
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           values: {
             example: '600px',
           },
@@ -306,19 +306,19 @@ it('should be possible to compare arbitrary variants and hardcoded variants', ()
   return run(input, config).then((result) => {
     expect(result.css).toMatchFormattedCss(css`
       @media (min-width: 500px) {
-        .min-\[500px\]\:italic {
+        .testmin-\[500px\]\:italic {
           font-style: italic;
         }
       }
 
       @media (min-width: 600px) {
-        .min-example\:italic {
+        .testmin-example\:italic {
           font-style: italic;
         }
       }
 
       @media (min-width: 700px) {
-        .min-\[700px\]\:italic {
+        .testmin-\[700px\]\:italic {
           font-style: italic;
         }
       }
@@ -333,13 +333,13 @@ it('should be possible to sort stacked arbitrary variants correctly', () => {
         raw: html`
           <div>
             <!-- 4 -->
-            <div class="min-[150px]:max-[400px]:underline"></div>
+            <div class="testmin-[150px]:testmax-[400px]:underline"></div>
             <!-- 2 -->
-            <div class="min-[100px]:max-[350px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[350px]:underline"></div>
             <!-- 1 -->
-            <div class="min-[100px]:max-[300px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[300px]:underline"></div>
             <!-- 3 -->
-            <div class="min-[100px]:max-[400px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[400px]:underline"></div>
           </div>
         `,
       },
@@ -347,13 +347,13 @@ it('should be possible to sort stacked arbitrary variants correctly', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           sort(a, z) {
             return parseInt(a.value) - parseInt(z.value)
           },
         })
 
-        matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
+        matchVariant('testmax', (value) => `@media (max-width: ${value})`, {
           sort(a, z) {
             return parseInt(z.value) - parseInt(a.value)
           },
@@ -370,17 +370,17 @@ it('should be possible to sort stacked arbitrary variants correctly', () => {
     expect(result.css).toMatchFormattedCss(css`
       @media (min-width: 100px) {
         @media (max-width: 400px) {
-          .min-\[100px\]\:max-\[400px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[400px\]\:underline {
             text-decoration-line: underline;
           }
         }
         @media (max-width: 350px) {
-          .min-\[100px\]\:max-\[350px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[350px\]\:underline {
             text-decoration-line: underline;
           }
         }
         @media (max-width: 300px) {
-          .min-\[100px\]\:max-\[300px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[300px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -388,7 +388,7 @@ it('should be possible to sort stacked arbitrary variants correctly', () => {
 
       @media (min-width: 150px) {
         @media (max-width: 400px) {
-          .min-\[150px\]\:max-\[400px\]\:underline {
+          .testmin-\[150px\]\:testmax-\[400px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -403,8 +403,8 @@ it('should maintain sort from other variants, if sort functions of arbitrary var
       {
         raw: html`
           <div>
-            <div class="min-[100px]:max-[200px]:focus:underline"></div>
-            <div class="min-[100px]:max-[200px]:hover:underline"></div>
+            <div class="testmin-[100px]:testmax-[200px]:focus:underline"></div>
+            <div class="testmin-[100px]:testmax-[200px]:hover:underline"></div>
           </div>
         `,
       },
@@ -412,13 +412,13 @@ it('should maintain sort from other variants, if sort functions of arbitrary var
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           sort(a, z) {
             return parseInt(a.value) - parseInt(z.value)
           },
         })
 
-        matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
+        matchVariant('testmax', (value) => `@media (max-width: ${value})`, {
           sort(a, z) {
             return parseInt(z.value) - parseInt(a.value)
           },
@@ -435,10 +435,10 @@ it('should maintain sort from other variants, if sort functions of arbitrary var
     expect(result.css).toMatchFormattedCss(css`
       @media (min-width: 100px) {
         @media (max-width: 200px) {
-          .min-\[100px\]\:max-\[200px\]\:hover\:underline:hover {
+          .testmin-\[100px\]\:testmax-\[200px\]\:hover\:underline:hover {
             text-decoration-line: underline;
           }
-          .min-\[100px\]\:max-\[200px\]\:focus\:underline:focus {
+          .testmin-\[100px\]\:testmax-\[200px\]\:focus\:underline:focus {
             text-decoration-line: underline;
           }
         }
@@ -453,10 +453,10 @@ it('should sort arbitrary variants left to right (1)', () => {
       {
         raw: html`
           <div>
-            <div class="min-[200px]:max-[400px]:underline"></div>
-            <div class="min-[200px]:max-[300px]:underline"></div>
-            <div class="min-[100px]:max-[400px]:underline"></div>
-            <div class="min-[100px]:max-[300px]:underline"></div>
+            <div class="testmin-[200px]:testmax-[400px]:underline"></div>
+            <div class="testmin-[200px]:testmax-[300px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[400px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[300px]:underline"></div>
           </div>
         `,
       },
@@ -464,12 +464,12 @@ it('should sort arbitrary variants left to right (1)', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           sort(a, z) {
             return parseInt(a.value) - parseInt(z.value)
           },
         })
-        matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
+        matchVariant('testmax', (value) => `@media (max-width: ${value})`, {
           sort(a, z) {
             return parseInt(z.value) - parseInt(a.value)
           },
@@ -486,13 +486,13 @@ it('should sort arbitrary variants left to right (1)', () => {
     expect(result.css).toMatchFormattedCss(css`
       @media (min-width: 100px) {
         @media (max-width: 400px) {
-          .min-\[100px\]\:max-\[400px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[400px\]\:underline {
             text-decoration-line: underline;
           }
         }
 
         @media (max-width: 300px) {
-          .min-\[100px\]\:max-\[300px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[300px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -500,13 +500,13 @@ it('should sort arbitrary variants left to right (1)', () => {
 
       @media (min-width: 200px) {
         @media (max-width: 400px) {
-          .min-\[200px\]\:max-\[400px\]\:underline {
+          .testmin-\[200px\]\:testmax-\[400px\]\:underline {
             text-decoration-line: underline;
           }
         }
 
         @media (max-width: 300px) {
-          .min-\[200px\]\:max-\[300px\]\:underline {
+          .testmin-\[200px\]\:testmax-\[300px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -521,10 +521,10 @@ it('should sort arbitrary variants left to right (2)', () => {
       {
         raw: html`
           <div>
-            <div class="max-[400px]:min-[200px]:underline"></div>
-            <div class="max-[300px]:min-[200px]:underline"></div>
-            <div class="max-[400px]:min-[100px]:underline"></div>
-            <div class="max-[300px]:min-[100px]:underline"></div>
+            <div class="testmax-[400px]:testmin-[200px]:underline"></div>
+            <div class="testmax-[300px]:testmin-[200px]:underline"></div>
+            <div class="testmax-[400px]:testmin-[100px]:underline"></div>
+            <div class="testmax-[300px]:testmin-[100px]:underline"></div>
           </div>
         `,
       },
@@ -532,12 +532,12 @@ it('should sort arbitrary variants left to right (2)', () => {
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           sort(a, z) {
             return parseInt(a.value) - parseInt(z.value)
           },
         })
-        matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
+        matchVariant('testmax', (value) => `@media (max-width: ${value})`, {
           sort(a, z) {
             return parseInt(z.value) - parseInt(a.value)
           },
@@ -554,12 +554,12 @@ it('should sort arbitrary variants left to right (2)', () => {
     expect(result.css).toMatchFormattedCss(css`
       @media (max-width: 400px) {
         @media (min-width: 100px) {
-          .max-\[400px\]\:min-\[100px\]\:underline {
+          .testmax-\[400px\]\:testmin-\[100px\]\:underline {
             text-decoration-line: underline;
           }
         }
         @media (min-width: 200px) {
-          .max-\[400px\]\:min-\[200px\]\:underline {
+          .testmax-\[400px\]\:testmin-\[200px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -567,12 +567,12 @@ it('should sort arbitrary variants left to right (2)', () => {
 
       @media (max-width: 300px) {
         @media (min-width: 100px) {
-          .max-\[300px\]\:min-\[100px\]\:underline {
+          .testmax-\[300px\]\:testmin-\[100px\]\:underline {
             text-decoration-line: underline;
           }
         }
         @media (min-width: 200px) {
-          .max-\[300px\]\:min-\[200px\]\:underline {
+          .testmax-\[300px\]\:testmin-\[200px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -587,10 +587,10 @@ it('should guarantee that we are not passing values from other variants to the w
       {
         raw: html`
           <div>
-            <div class="min-[200px]:max-[400px]:underline"></div>
-            <div class="min-[200px]:max-[300px]:underline"></div>
-            <div class="min-[100px]:max-[400px]:underline"></div>
-            <div class="min-[100px]:max-[300px]:underline"></div>
+            <div class="testmin-[200px]:testmax-[400px]:underline"></div>
+            <div class="testmin-[200px]:testmax-[300px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[400px]:underline"></div>
+            <div class="testmin-[100px]:testmax-[300px]:underline"></div>
           </div>
         `,
       },
@@ -598,7 +598,7 @@ it('should guarantee that we are not passing values from other variants to the w
     corePlugins: { preflight: false },
     plugins: [
       ({ matchVariant }) => {
-        matchVariant('min', ({ value }) => `@media (min-width: ${value})`, {
+        matchVariant('testmin', (value) => `@media (min-width: ${value})`, {
           sort(a, z) {
             let lookup = ['100px', '200px']
             if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) {
@@ -607,7 +607,7 @@ it('should guarantee that we are not passing values from other variants to the w
             return lookup.indexOf(a.value) - lookup.indexOf(z.value)
           },
         })
-        matchVariant('max', ({ value }) => `@media (max-width: ${value})`, {
+        matchVariant('testmax', (value) => `@media (max-width: ${value})`, {
           sort(a, z) {
             let lookup = ['300px', '400px']
             if (lookup.indexOf(a.value) === -1 || lookup.indexOf(z.value) === -1) {
@@ -628,13 +628,13 @@ it('should guarantee that we are not passing values from other variants to the w
     expect(result.css).toMatchFormattedCss(css`
       @media (min-width: 100px) {
         @media (max-width: 400px) {
-          .min-\[100px\]\:max-\[400px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[400px\]\:underline {
             text-decoration-line: underline;
           }
         }
 
         @media (max-width: 300px) {
-          .min-\[100px\]\:max-\[300px\]\:underline {
+          .testmin-\[100px\]\:testmax-\[300px\]\:underline {
             text-decoration-line: underline;
           }
         }
@@ -642,13 +642,13 @@ it('should guarantee that we are not passing values from other variants to the w
 
       @media (min-width: 200px) {
         @media (max-width: 400px) {
-          .min-\[200px\]\:max-\[400px\]\:underline {
+          .testmin-\[200px\]\:testmax-\[400px\]\:underline {
             text-decoration-line: underline;
           }
         }
 
         @media (max-width: 300px) {
-          .min-\[200px\]\:max-\[300px\]\:underline {
+          .testmin-\[200px\]\:testmax-\[300px\]\:underline {
             text-decoration-line: underline;
           }
         }
diff --git a/tests/min-max-screen-variants.test.js b/tests/min-max-screen-variants.test.js
new file mode 100644
index 00000000..cb6a0629
--- /dev/null
+++ b/tests/min-max-screen-variants.test.js
@@ -0,0 +1,662 @@
+import { run, css, html } from './util/run'
+
+let warn
+
+beforeEach(() => {
+  let log = require('../src/util/log')
+  warn = jest.spyOn(log.default, 'warn')
+})
+
+afterEach(() => {
+  warn.mockClear()
+})
+
+let defaultScreens = {
+  sm: '640px',
+  md: '768px',
+  lg: '1024px',
+  xl: '1280px',
+  '2xl': '1536px',
+}
+
+it('sorts min and max correctly relative to screens and each other', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="font-bold max-[800px]:font-bold max-[700px]:font-bold sm:font-bold min-[700px]:font-bold md:font-bold min-[800px]:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: defaultScreens,
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .font-bold {
+        font-weight: 700;
+      }
+      @media (max-width: 800px) {
+        .max-\[800px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (max-width: 700px) {
+        .max-\[700px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 640px) {
+        .sm\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 700px) {
+        .min-\[700px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 768px) {
+        .md\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 800px) {
+        .min-\[800px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+    `)
+  })
+})
+
+it('works when using min variants screens config is empty and variants all use the same unit', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="font-bold sm:font-bold min-[700px]:font-bold md:font-bold min-[800px]:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {},
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 700px) {
+      .min-\[700px\]\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 800px) {
+      .min-\[800px\]\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(0)
+})
+
+it('works when using max variants screens config is empty and variants all use the same unit', () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="font-bold max-[800px]:font-bold max-[700px]:font-bold sm:font-bold md:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {},
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .font-bold {
+        font-weight: 700;
+      }
+      @media (max-width: 800px) {
+        .max-\[800px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (max-width: 700px) {
+        .max-\[700px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+    `)
+  })
+})
+
+it('converts simple min-width screens for max variant', () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="font-bold max-lg:font-bold max-[700px]:font-bold max-sm:font-bold max-[300px]:font-bold sm:font-bold md:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: defaultScreens,
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .font-bold {
+        font-weight: 700;
+      }
+      @media not all and (min-width: 1024px) {
+        .max-lg\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (max-width: 700px) {
+        .max-\[700px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media not all and (min-width: 640px) {
+        .max-sm\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (max-width: 300px) {
+        .max-\[300px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 640px) {
+        .sm\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 768px) {
+        .md\:font-bold {
+          font-weight: 700;
+        }
+      }
+    `)
+  })
+})
+
+it('does not have keyed screens for min variant', () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="min-sm:font-bold min-lg:font-bold font-bold min-[300px]:font-bold sm:font-bold min-[700px]:font-bold md:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: defaultScreens,
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .font-bold {
+        font-weight: 700;
+      }
+      @media (min-width: 300px) {
+        .min-\[300px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 640px) {
+        .sm\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 700px) {
+        .min-\[700px\]\:font-bold {
+          font-weight: 700;
+        }
+      }
+      @media (min-width: 768px) {
+        .md\:font-bold {
+          font-weight: 700;
+        }
+      }
+    `)
+  })
+})
+
+it('warns when using min variants with complex screen configs', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="font-bold sm:font-bold min-[700px]:font-bold md:font-bold"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {
+        sm: '640px',
+        md: '768px',
+        lg: '1024px',
+        xl: '1280px',
+        '2xl': '1536px',
+
+        // Any presence of an object makes it complex
+        yodawg: { min: '700px' },
+      },
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 640px) {
+      .sm\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 768px) {
+      .md\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['complex-screen-config'])
+})
+
+it('warns when using min variants with simple configs containing mixed units', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="font-bold sm:font-bold min-[700px]:font-bold md:font-bold"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {
+        sm: '640px',
+        md: '48rem',
+        lg: '1024px',
+        xl: '1280px',
+        '2xl': '1536px',
+      },
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 640px) {
+      .sm\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 48rem) {
+      .md\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['mixed-screen-units'])
+})
+
+it('warns when using min variants with mixed units (with screens config)', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="min-[700rem]:font-bold font-bold sm:font-bold md:font-bold"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {
+        sm: '640px',
+        md: '768px',
+        lg: '1024px',
+        xl: '1280px',
+        '2xl': '1536px',
+      },
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 640px) {
+      .sm\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 768px) {
+      .md\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units'])
+})
+
+it('warns when using min variants with mixed units (with no screens config)', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="min-[700rem]:font-bold font-bold sm:font-bold min-[700px]:font-bold md:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {},
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 700rem) {
+      .min-\[700rem\]\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units'])
+})
+
+it('warns when using max variants with complex screen configs', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="font-bold max-[700px]:font-bold sm:font-bold md:font-bold"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {
+        sm: '640px',
+        md: '768px',
+        lg: '1024px',
+        xl: '1280px',
+        '2xl': '1536px',
+
+        // Any presence of an object makes it complex
+        yodawg: { min: '700px' },
+      },
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 640px) {
+      .sm\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 768px) {
+      .md\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['complex-screen-config'])
+})
+
+it('warns when using max variants with simple configs containing mixed units', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="font-bold max-[700px]:font-bold sm:font-bold md:font-bold"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {
+        sm: '640px',
+        md: '48rem',
+        lg: '1024px',
+        xl: '1280px',
+        '2xl': '1536px',
+      },
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 640px) {
+      .sm\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 48rem) {
+      .md\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['mixed-screen-units'])
+})
+
+it('warns when using max variants with mixed units (with screens config)', async () => {
+  let config = {
+    content: [
+      {
+        raw: html` <div class="max-[700rem]:font-bold font-bold sm:font-bold md:font-bold"></div> `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {
+        sm: '640px',
+        md: '768px',
+        lg: '1024px',
+        xl: '1280px',
+        '2xl': '1536px',
+      },
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (min-width: 640px) {
+      .sm\:font-bold {
+        font-weight: 700;
+      }
+    }
+    @media (min-width: 768px) {
+      .md\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units'])
+})
+
+it('warns when using max variants with mixed units (with no screens config)', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="max-[700rem]:font-bold font-bold max-[700px]:font-bold sm:font-bold md:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {},
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (max-width: 700rem) {
+      .max-\[700rem\]\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units'])
+})
+
+it('warns when using min and max variants with mixed units (with no screens config)', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div
+            class="max-[700rem]:font-bold font-bold sm:font-bold min-[700px]:font-bold md:font-bold"
+          ></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {
+      screens: {},
+    },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  let result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .font-bold {
+      font-weight: 700;
+    }
+    @media (max-width: 700rem) {
+      .max-\[700rem\]\:font-bold {
+        font-weight: 700;
+      }
+    }
+  `)
+
+  expect(warn).toHaveBeenCalledTimes(1)
+  expect(warn.mock.calls.map((x) => x[0])).toEqual(['minmax-have-mixed-units'])
+})
diff --git a/tests/normalize-screens.test.js b/tests/normalize-screens.test.js
index 98fc22c5..da31c50d 100644
--- a/tests/normalize-screens.test.js
+++ b/tests/normalize-screens.test.js
@@ -4,8 +4,8 @@ it('should normalize an array of string values', () => {
   let screens = ['768px', '1200px']
 
   expect(normalizeScreens(screens)).toEqual([
-    { name: '768px', values: [{ min: '768px', max: undefined }] },
-    { name: '1200px', values: [{ min: '1200px', max: undefined }] },
+    { name: '768px', not: false, values: [{ min: '768px', max: undefined }] },
+    { name: '1200px', not: false, values: [{ min: '1200px', max: undefined }] },
   ])
 })
 
@@ -16,8 +16,8 @@ it('should normalize an object with string values', () => {
   }
 
   expect(normalizeScreens(screens)).toEqual([
-    { name: 'a', values: [{ min: '768px', max: undefined }] },
-    { name: 'b', values: [{ min: '1200px', max: undefined }] },
+    { name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
+    { name: 'b', not: false, values: [{ min: '1200px', max: undefined }] },
   ])
 })
 
@@ -28,8 +28,8 @@ it('should normalize an object with object values', () => {
   }
 
   expect(normalizeScreens(screens)).toEqual([
-    { name: 'a', values: [{ min: '768px', max: undefined }] },
-    { name: 'b', values: [{ min: undefined, max: '1200px' }] },
+    { name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
+    { name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] },
   ])
 })
 
@@ -41,6 +41,7 @@ it('should normalize an object with multiple object values', () => {
   expect(normalizeScreens(screens)).toEqual([
     {
       name: 'a',
+      not: false,
       values: [
         { max: undefined, min: '768px', raw: undefined },
         { max: '1200px', min: undefined, raw: undefined },
@@ -56,7 +57,7 @@ it('should normalize an object with object values (min-width normalized to width
   }
 
   expect(normalizeScreens(screens)).toEqual([
-    { name: 'a', values: [{ min: '768px', max: undefined }] },
-    { name: 'b', values: [{ min: undefined, max: '1200px' }] },
+    { name: 'a', not: false, values: [{ min: '768px', max: undefined }] },
+    { name: 'b', not: false, values: [{ min: undefined, max: '1200px' }] },
   ])
 })