Repository: tailwindlabs/tailwindcss
Pull request #9558 (2022-10-14 17:59:25)

Add new `min` and `max` variants
This PR introduces three new pieces of functionality related to screen variants.

> **Note** These features all require a "simple" screens config. Please see the [restrictions below](#restrictions) on when they are enabled and what you can and can't do.

# New Features

## Sort screens by value (instead of config order)

Currently, screens are sorted by the order they appear in your `tailwind.config.js` file. This is great until you want to add your own `xs` variant through `extends`. Right now if you do that Tailwind will place the `xs` variant at the end of your screens list and at the end of your CSS, causing `xs` to incorrectly take precedence over higher screen variants / sizes.

To work around this you must duplicate your screens list and place `xs` at the beginning:

```js
module.exports = {
  theme: {
    screens: {
      xs: "320px",
      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
    },
  },
};
```

Now, with this change, you can use `extends` and it will work as expected:

```js
module.exports = {
  theme: {
    extend: {
      screens: {
        xs: "320px",
      },
    },
  },
};
```

With the above config and this HTML:

```html
<div class="xs:px-2 sm:px-4"></div>
```

This is now the output:

```css
@media (min-width: 320px) {
  .xs\:px-2 {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
}

@media (min-width: 640px) {
  .sm\:px-4 {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}
```

## Adds a `max-*` variant for `max-width` screen sizes

Currently, it is possible to define your own `max-width` screen sizes in the config by adding more screens in the appropriate order:

```js
module.exports = {
  theme: {
    screens: {
      "max-2xl": { max: "1536px" },
      "max-xl": { max: "1280px" },
      "max-lg": { max: "1024px" },
      "max-md": { max: "768px" },
      "max-sm": { max: "640px" },

      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
    },
  },
};
```

But this solution has some drawbacks:

1. These new `max-width` screens MUST appear in descending order. Not doing so would cause `max-2xl` to override the all the other `max-width` screens since screens that are 480px wide, 768px wide, etc… are all also at most 1536px wide.
2. You can't use `extends` to add them because they should be placed before `min-width` screens for optimal behavior.
3. Because of the above, you have to duplicate your entire screen list.

This new feature addresses all of these shortcomings for simple, `min-width`-based screen configs.

Better yet, **you do not have to add more screens yourself** — they're automatically supported!

Now, without modifying your screens _at all_ you'll be able to write this:

```html
<div class="px-5 max-sm:px-2 sm:px-4"></div>
```

And this is the output:

```css
.px-5 {
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}

@media not all and (min-width: 640px) {
  .max-sm\:px-2 {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
}

@media (min-width: 640px) {
  .sm\:px-4 {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}
```

Further, you can combine standard screen variants and the new max variant to produce a "between" or range variant for your utility:

```html
<div class="sm:max-md:px-2"></div>
```

```css
@media (min-width: 640px) {
  @media not all and (min-width: 768px) {
    .sm\:max-md\:px-2 {
      padding-left: 0.5rem;
      padding-right: 0.5rem;
    }
  }
}
```

Which means, when the viewport is at least `640px` and less than `768px`, the padding will be applied.

## The `max-*` variant also supports arbitrary values

And, because it utilizes the new `matchVariant` feature in v3.2+ the `max-*` variant supports arbitrary values, not just screen names. This is great for one-off cases when it doesn't make sense to add an additional screen to your config. This means you can do things like this:

```html
<div class="max-[320px]:px-2 max-sm:px-4 px-5"></div>
```

And this is the output:

```css
.px-5 {
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}

@media not all and (min-width: 480px) {
  .max-sm\:px-4 {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}

@media not all and (min-width: 320px) {
  .max-\[320px\]\:px-2 {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
}
```

## Adds a `min-[]` variant specifically for arbitrary values

Tailwind's screens are all already `min-width` by default. And this holds true for most projects that customize their screens config to add additional screen sizes. This means that, normally, there's no need for a `min` variant.

But, wait, we just added arbitrary values to the `max-*` variant. If you can do that for `max-width` screen sizes it stands to reason that you'd want to do this for `min-width` screen sizes as well.

Introducing the `min-[]` variant!

You can now do this:

```html
<div class="min-[320px]:px-2 px-5"></div>
```

And this is the output:

```css
.px-5 {
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}
@media (min-width: 320px) {
  .min-\[320px\]\:px-2 {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
}
```

Even better, these are sorted in ascending order alongside other screen variants in your project's config since they are also `min-width`-based:

```html
<div class="min-[320px]:px-2 sm:px-4 px-5"></div>
```

Which produces this output:

```css
.px-5 {
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}
@media (min-width: 320px) {
  .min-\[320px\]\:px-2 {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
  }
}
@media (min-width: 640px) {
  .sm\:px-4 {
    padding-left: 1rem;
    padding-right: 1rem;
  }
}
```

> **Note:** The `min-[]` variant is only available for arbitrary values and cannot be used with named screen sizes.

# Restrictions

## Your screens, if customized, MUST be `min-width` only and string values

To ensure proper sorting we've made the decision to restrict the use of these features to screens that are "simple", meaning that they are `min-width`-based, and consist of only string values that use the same units.

For example, these are all valid:

```js
module.exports = {
  theme: {
    screens: {
      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
    },
  },
};
```

```js
module.exports = {
  theme: {
    extends: {
      screens: {
        xs: "320px",
        reallybig: "3440px",
      },
    },
  },
};
```

While these are invalid and will disable these new features:

```js
module.exports = {
  theme: {
    screens: {
      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
      portrait: { raw: "(orientation: portrait)" },
    },
  },
};
```

```js
module.exports = {
  theme: {
    extends: {
      screens: {
        xs: { min: "320px" },
      },
    },
  },
};
```

```js
module.exports = {
  theme: {
    extends: {
      screens: {
        xs: { max: "320px" },
      },
    },
  },
};
```

```js
module.exports = {
  theme: {
    extends: {
      screens: {
        xs: { min: "320px", max: "479px" },
      },
    },
  },
};
```

<small>_psst if you're using custom screens in your project for landscape, portrait, or print media queries we have variants for those already!_</small>

## All screens including both built-in and custom ones MUST all use the same units

For example, these are all valid:

```js
module.exports = {
  theme: {
    screens: {
      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
    },
  },
};
```

```js
module.exports = {
  theme: {
    screens: {
      sm: "40rem",
      md: "48rem",
      lg: "64rem",
      xl: "80rem",
      "2xl": "96rem",
    },
  },
};
```

While these are invalid and will disable **all** of these new features together:

```js
module.exports = {
  theme: {
    screens: {
      sm: "640px",
      md: "48rem", // See how we're using rems here instead of px
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
    },
  },
};
```

```js
module.exports = {
  theme: {
    extends: {
      screens: {
        md: "48rem", // Screens internally are px-based so we can't use rem's here
      },
    },
  },
};
```

## All uses of `min` and `max` MUST use the same units (including built in and custom screens)

To ensure proper sorting we've made the decision to restrict the use of `min` and `max` to the same units. This means you can't do any of these — across your whole project:

```html
<div class="min-[320px]:px-2 max-[640rem]:px-4"></div>
<div class="min-[320px]:px-2 min-[48rem]:px-4"></div>
<div class="max-[320px]:px-2 max-[48rem]:px-4"></div>
```

If you want to use `rem` for your `min` and `max` values you'll need to use `rem` for all of your screens as well.

# tl;dr

As long as your screens config is "simple" (see restrictions above):

1. You can now add screens via `extends` and they'll be properly sorted among your other screens.
2. Adds a new `min-[]` variant that allows you to use arbitrary values with `min-width` media queries which are sorted in ascending order alongside your other screens.
3. Adds a new `max-*` variant, which supports both your screens config and arbitrary values and are sorted in descending order and **all of them are before your min-width screens**.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c149d888..39b41db4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - 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))
 
 ### Fixed
 
diff --git a/src/corePlugins.js b/src/corePlugins.js
index 0ae64d9b..0881a63d 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'
@@ -220,12 +225,131 @@ 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 }) => {
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index 2f0ca840..47e4f6eb 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -560,7 +560,9 @@ 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')
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/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 }
 }


Test patch:
diff --git a/tests/match-variants.test.js b/tests/match-variants.test.js
index fad17193..7da1b423 100644
--- a/tests/match-variants.test.js
+++ b/tests/match-variants.test.js
@@ -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' }] },
   ])
 })