Repository: tailwindlabs/tailwindcss
Pull request #6104 (2021-11-16 13:42:57)

Add tuple syntax to guarantee screens order
This PR will add a new tuple syntax for the screens configuration.

This is important because otherwise we can't guarantee the order (when using the object syntax).

```js
module.exports = {
  // ...
  theme: {
    extend: {
      screens: {
        1800: { max: '1800px' },
        1200: { max: '1200px' },
        768: { max: '768px' },
      }
    }
  }
}
```
This would result in `768` to be at the top, but we want this at the very bottom, just like it is defined in the config.

Now you can do that using the `tuple` syntax:

```js
module.exports = {
  // ...
  theme: {
    extend: {
      screens: [
        [1800, { max: '1800px' }],
        [1200, { max: '1200px' }],
        [768, { max: '768px' }],
      ],
    }
  }
}
```

This is now defined as an array, which means that the order _is_ guaranteed.

While working on the PR, I noticed that we have a few different ways to configure the screens object. This means that everywhere you used it that you have to keep all syntaxes into account. Now adding another option results in updating all of those places. So instead I've added a `normalizeScreens` function that normalizes it into a single output regardless of the input.

Another reason why this took a bit longer than expected is that this is used as a fallback to the `container.screens` options as well. There are also other cases where this is used, for example when using the `@screen` directive in your css.

Closes: #5956

<!--

👋 Hey, thanks for your interest in contributing to Tailwind!

**Please ask first before starting work on any significant new features.**

It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create an issue to first discuss any significant new features. This includes things like adding new utilities, creating new at-rules, or adding new component examples to the documentation.

https://github.com/tailwindcss/tailwindcss/blob/master/.github/CONTRIBUTING.md

-->



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15756dfd..d1965c00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-Nothing yet!
-
 ### Fixed
 
 - Enforce the order of some variants (like `before` and `after`) ([#6018](https://github.com/tailwindlabs/tailwindcss/pull/6018))
@@ -16,6 +14,7 @@ Nothing yet!
 ### Added
 
 - Add `placeholder` variant ([#6106](https://github.com/tailwindlabs/tailwindcss/pull/6106))
+- Add tuple syntax for configuring screens while guaranteeing order ([#5956](https://github.com/tailwindlabs/tailwindcss/pull/5956))
 
 ## [3.0.0-alpha.2] - 2021-11-08
 
diff --git a/src/corePlugins.js b/src/corePlugins.js
index dc76e279..83135827 100644
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -11,6 +11,7 @@ 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 { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue'
 
 export let variantPlugins = {
@@ -158,11 +159,10 @@ export let variantPlugins = {
   },
 
   screenVariants: ({ theme, addVariant }) => {
-    for (let screen in theme('screens')) {
-      let size = theme('screens')[screen]
-      let query = buildMediaQuery(size)
+    for (let screen of normalizeScreens(theme('screens'))) {
+      let query = buildMediaQuery(screen)
 
-      addVariant(screen, `@media ${query}`)
+      addVariant(screen.name, `@media ${query}`)
     }
   },
 }
@@ -182,24 +182,10 @@ export let corePlugins = {
   },
 
   container: (() => {
-    function extractMinWidths(breakpoints) {
-      return Object.values(breakpoints ?? {}).flatMap((breakpoints) => {
-        if (typeof breakpoints === 'string') {
-          breakpoints = { min: breakpoints }
-        }
-
-        if (!Array.isArray(breakpoints)) {
-          breakpoints = [breakpoints]
-        }
-
-        return breakpoints
-          .filter((breakpoint) => {
-            return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width')
-          })
-          .map((breakpoint) => {
-            return breakpoint['min-width'] ?? breakpoint.min
-          })
-      })
+    function extractMinWidths(breakpoints = []) {
+      return breakpoints
+        .flatMap((breakpoint) => breakpoint.values.map((breakpoint) => breakpoint.min))
+        .filter((v) => v !== undefined)
     }
 
     function mapMinWidthsToPadding(minWidths, screens, paddings) {
@@ -228,16 +214,11 @@ export let corePlugins = {
       }
 
       for (let minWidth of minWidths) {
-        for (let [screen, value] of Object.entries(screens)) {
-          let screenMinWidth =
-            typeof value === 'object' && value !== null ? value.min || value['min-width'] : value
-
-          if (`${screenMinWidth}` === `${minWidth}`) {
-            mapping.push({
-              screen,
-              minWidth,
-              padding: paddings[screen],
-            })
+        for (let screen of screens) {
+          for (let { min } of screen.values) {
+            if (min === minWidth) {
+              mapping.push({ minWidth, padding: paddings[screen.name] })
+            }
           }
         }
       }
@@ -246,12 +227,12 @@ export let corePlugins = {
     }
 
     return function ({ addComponents, theme }) {
-      let screens = theme('container.screens', theme('screens'))
+      let screens = normalizeScreens(theme('container.screens', theme('screens')))
       let minWidths = extractMinWidths(screens)
       let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
 
       let generatePaddingFor = (minWidth) => {
-        let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`)
+        let paddingConfig = paddings.find((padding) => padding.minWidth === minWidth)
 
         if (!paddingConfig) {
           return {}
diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js
index fb3474b2..778230e5 100644
--- a/src/lib/evaluateTailwindFunctions.js
+++ b/src/lib/evaluateTailwindFunctions.js
@@ -2,6 +2,7 @@ import dlv from 'dlv'
 import didYouMean from 'didyoumean'
 import transformThemeValue from '../util/transformThemeValue'
 import parseValue from 'postcss-value-parser'
+import { normalizeScreens } from '../util/normalizeScreens'
 import buildMediaQuery from '../util/buildMediaQuery'
 import { toPath } from '../util/toPath'
 
@@ -173,12 +174,14 @@ export default function ({ tailwindConfig: config }) {
     },
     screen: (node, screen) => {
       screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
+      let screens = normalizeScreens(config.theme.screens)
+      let screenDefinition = screens.find(({ name }) => name === screen)
 
-      if (config.theme.screens[screen] === undefined) {
+      if (!screenDefinition) {
         throw node.error(`The '${screen}' screen does not exist in your theme.`)
       }
 
-      return buildMediaQuery(config.theme.screens[screen])
+      return buildMediaQuery(screenDefinition)
     },
   }
   return (root) => {
diff --git a/src/lib/substituteScreenAtRules.js b/src/lib/substituteScreenAtRules.js
index 1b61d38f..5a45cff2 100644
--- a/src/lib/substituteScreenAtRules.js
+++ b/src/lib/substituteScreenAtRules.js
@@ -1,16 +1,19 @@
+import { normalizeScreens } from '../util/normalizeScreens'
 import buildMediaQuery from '../util/buildMediaQuery'
 
 export default function ({ tailwindConfig: { theme } }) {
   return function (css) {
     css.walkAtRules('screen', (atRule) => {
-      const screen = atRule.params
+      let screen = atRule.params
+      let screens = normalizeScreens(theme.screens)
+      let screenDefinition = screens.find(({ name }) => name === screen)
 
-      if (!theme.screens?.hasOwnProperty?.(screen)) {
+      if (!screenDefinition) {
         throw atRule.error(`No \`${screen}\` screen found.`)
       }
 
       atRule.name = 'media'
-      atRule.params = buildMediaQuery(theme.screens[screen])
+      atRule.params = buildMediaQuery(screenDefinition)
     })
   }
 }
diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js
index ba3b696e..ccfc63ba 100644
--- a/src/util/buildMediaQuery.js
+++ b/src/util/buildMediaQuery.js
@@ -1,24 +1,20 @@
 export default function buildMediaQuery(screens) {
-  if (typeof screens === 'string') {
-    screens = { min: screens }
-  }
-
-  if (!Array.isArray(screens)) {
-    screens = [screens]
-  }
+  screens = Array.isArray(screens) ? screens : [screens]
 
   return screens
-    .map((screen) => {
-      if (screen?.hasOwnProperty?.('raw')) {
-        return screen.raw
-      }
+    .map((screen) =>
+      screen.values.map((screen) => {
+        if (screen.raw !== undefined) {
+          return screen.raw
+        }
 
-      return Object.entries(screen)
-        .map(([feature, value]) => {
-          feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
-          return `(${feature}: ${value})`
-        })
-        .join(' and ')
-    })
+        return [
+          screen.min && `(min-width: ${screen.min})`,
+          screen.max && `(max-width: ${screen.max})`,
+        ]
+          .filter(Boolean)
+          .join(' and ')
+      })
+    )
     .join(', ')
 }
diff --git a/src/util/normalizeScreens.js b/src/util/normalizeScreens.js
new file mode 100644
index 00000000..e7bcefe6
--- /dev/null
+++ b/src/util/normalizeScreens.js
@@ -0,0 +1,42 @@
+/**
+ * A function that normalizes the various forms that the screens object can be
+ * provided in.
+ *
+ * Input(s):
+ *   - ['100px', '200px'] // Raw strings
+ *   - { sm: '100px', md: '200px' } // Object with string values
+ *   - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
+ *   - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
+ *   - [['sm', '100px'], ['md', '200px']] // Tuple object
+ *
+ * Output(s):
+ *   - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
+ */
+export function normalizeScreens(screens) {
+  if (Array.isArray(screens)) {
+    return screens.map((screen) => {
+      if (typeof screen === 'string') {
+        return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
+      }
+
+      let [name, options] = screen
+      name = name.toString()
+
+      if (typeof options === 'string') {
+        return { name, values: [{ min: options, max: undefined }] }
+      }
+
+      if (Array.isArray(options)) {
+        return { name, values: options.map((option) => resolveValue(option)) }
+      }
+
+      return { name, values: [resolveValue(options)] }
+    })
+  }
+
+  return normalizeScreens(Object.entries(screens ?? {}))
+}
+
+function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
+  return { min, max, raw }
+}


Test patch:
diff --git a/tests/containerPlugin.test.js b/tests/containerPlugin.test.js
index 51b7093b..7dfc7352 100644
--- a/tests/containerPlugin.test.js
+++ b/tests/containerPlugin.test.js
@@ -343,3 +343,39 @@ test('container can use variants', () => {
     `)
   })
 })
+
+test('container can use screens with tuple syntax', () => {
+  let config = {
+    content: [{ raw: html`<div class="container"></div>` }],
+    theme: {
+      screens: [
+        [1800, { min: '1800px' }],
+        [1200, { min: '1200px' }],
+        [768, { min: '768px' }],
+      ],
+    },
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .container {
+        width: 100%;
+      }
+      @media (min-width: 768px) {
+        .container {
+          max-width: 768px;
+        }
+      }
+      @media (min-width: 1200px) {
+        .container {
+          max-width: 1200px;
+        }
+      }
+      @media (min-width: 1800px) {
+        .container {
+          max-width: 1800px;
+        }
+      }
+    `)
+  })
+})
diff --git a/tests/evaluateTailwindFunctions.test.js b/tests/evaluateTailwindFunctions.test.js
index b2c69a0a..01d7393b 100644
--- a/tests/evaluateTailwindFunctions.test.js
+++ b/tests/evaluateTailwindFunctions.test.js
@@ -1,17 +1,22 @@
 import postcss from 'postcss'
 import plugin from '../src/lib/evaluateTailwindFunctions'
+import { css } from './util/run'
 
 function run(input, opts = {}) {
   return postcss([plugin({ tailwindConfig: opts })]).process(input, { from: undefined })
 }
 
 test('it looks up values in the theme using dot notation', () => {
-  const input = `
-    .banana { color: theme('colors.yellow'); }
+  let input = css`
+    .banana {
+      color: theme('colors.yellow');
+    }
   `
 
-  const output = `
-    .banana { color: #f7cc50; }
+  let output = css`
+    .banana {
+      color: #f7cc50;
+    }
   `
 
   return run(input, {
@@ -27,37 +32,85 @@ test('it looks up values in the theme using dot notation', () => {
 })
 
 test('color can be a function', () => {
-  const input = `
-    .backgroundColor { color: theme('backgroundColor.fn') }
-    .borderColor { color: theme('borderColor.fn') }
-    .caretColor { color: theme('caretColor.fn') }
-    .colors { color: theme('colors.fn') }
-    .divideColor { color: theme('divideColor.fn') }
-    .fill { color: theme('fill.fn') }
-    .gradientColorStops { color: theme('gradientColorStops.fn') }
-    .placeholderColor { color: theme('placeholderColor.fn') }
-    .ringColor { color: theme('ringColor.fn') }
-    .ringOffsetColor { color: theme('ringOffsetColor.fn') }
-    .stroke { color: theme('stroke.fn') }
-    .textColor { color: theme('textColor.fn') }
-  `
-
-  const output = `
-    .backgroundColor { color: #f00 }
-    .borderColor { color: #f00 }
-    .caretColor { color: #f00 }
-    .colors { color: #f00 }
-    .divideColor { color: #f00 }
-    .fill { color: #f00 }
-    .gradientColorStops { color: #f00 }
-    .placeholderColor { color: #f00 }
-    .ringColor { color: #f00 }
-    .ringOffsetColor { color: #f00 }
-    .stroke { color: #f00 }
-    .textColor { color: #f00 }
-  `
-
-  const fn = () => `#f00`
+  let input = css`
+    .backgroundColor {
+      color: theme('backgroundColor.fn');
+    }
+    .borderColor {
+      color: theme('borderColor.fn');
+    }
+    .caretColor {
+      color: theme('caretColor.fn');
+    }
+    .colors {
+      color: theme('colors.fn');
+    }
+    .divideColor {
+      color: theme('divideColor.fn');
+    }
+    .fill {
+      color: theme('fill.fn');
+    }
+    .gradientColorStops {
+      color: theme('gradientColorStops.fn');
+    }
+    .placeholderColor {
+      color: theme('placeholderColor.fn');
+    }
+    .ringColor {
+      color: theme('ringColor.fn');
+    }
+    .ringOffsetColor {
+      color: theme('ringOffsetColor.fn');
+    }
+    .stroke {
+      color: theme('stroke.fn');
+    }
+    .textColor {
+      color: theme('textColor.fn');
+    }
+  `
+
+  let output = css`
+    .backgroundColor {
+      color: #f00;
+    }
+    .borderColor {
+      color: #f00;
+    }
+    .caretColor {
+      color: #f00;
+    }
+    .colors {
+      color: #f00;
+    }
+    .divideColor {
+      color: #f00;
+    }
+    .fill {
+      color: #f00;
+    }
+    .gradientColorStops {
+      color: #f00;
+    }
+    .placeholderColor {
+      color: #f00;
+    }
+    .ringColor {
+      color: #f00;
+    }
+    .ringOffsetColor {
+      color: #f00;
+    }
+    .stroke {
+      color: #f00;
+    }
+    .textColor {
+      color: #f00;
+    }
+  `
+
+  let fn = () => `#f00`
 
   return run(input, {
     theme: {
@@ -81,12 +134,16 @@ test('color can be a function', () => {
 })
 
 test('quotes are optional around the lookup path', () => {
-  const input = `
-    .banana { color: theme(colors.yellow); }
+  let input = css`
+    .banana {
+      color: theme(colors.yellow);
+    }
   `
 
-  const output = `
-    .banana { color: #f7cc50; }
+  let output = css`
+    .banana {
+      color: #f7cc50;
+    }
   `
 
   return run(input, {
@@ -102,12 +159,16 @@ test('quotes are optional around the lookup path', () => {
 })
 
 test('a default value can be provided', () => {
-  const input = `
-    .cookieMonster { color: theme('colors.blue', #0000ff); }
+  let input = css`
+    .cookieMonster {
+      color: theme('colors.blue', #0000ff);
+    }
   `
 
-  const output = `
-    .cookieMonster { color: #0000ff; }
+  let output = css`
+    .cookieMonster {
+      color: #0000ff;
+    }
   `
 
   return run(input, {
@@ -123,12 +184,16 @@ test('a default value can be provided', () => {
 })
 
 test('the default value can use the theme function', () => {
-  const input = `
-    .cookieMonster { color: theme('colors.blue', theme('colors.yellow')); }
+  let input = css`
+    .cookieMonster {
+      color: theme('colors.blue', theme('colors.yellow'));
+    }
   `
 
-  const output = `
-    .cookieMonster { color: #f7cc50; }
+  let output = css`
+    .cookieMonster {
+      color: #f7cc50;
+    }
   `
 
   return run(input, {
@@ -144,12 +209,16 @@ test('the default value can use the theme function', () => {
 })
 
 test('quotes are preserved around default values', () => {
-  const input = `
-    .heading { font-family: theme('fontFamily.sans', "Helvetica Neue"); }
+  let input = css`
+    .heading {
+      font-family: theme('fontFamily.sans', 'Helvetica Neue');
+    }
   `
 
-  const output = `
-    .heading { font-family: "Helvetica Neue"; }
+  let output = css`
+    .heading {
+      font-family: 'Helvetica Neue';
+    }
   `
 
   return run(input, {
@@ -165,12 +234,16 @@ test('quotes are preserved around default values', () => {
 })
 
 test('an unquoted list is valid as a default value', () => {
-  const input = `
-    .heading { font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif); }
+  let input = css`
+    .heading {
+      font-family: theme('fontFamily.sans', Helvetica, Arial, sans-serif);
+    }
   `
 
-  const output = `
-    .heading { font-family: Helvetica, Arial, sans-serif; }
+  let output = css`
+    .heading {
+      font-family: Helvetica, Arial, sans-serif;
+    }
   `
 
   return run(input, {
@@ -186,8 +259,10 @@ test('an unquoted list is valid as a default value', () => {
 })
 
 test('a missing root theme value throws', () => {
-  const input = `
-    .heading { color: theme('colours.gray.100'); }
+  let input = css`
+    .heading {
+      color: theme('colours.gray.100');
+    }
   `
 
   return expect(
@@ -204,8 +279,10 @@ test('a missing root theme value throws', () => {
 })
 
 test('a missing nested theme property throws', () => {
-  const input = `
-    .heading { color: theme('colors.red'); }
+  let input = css`
+    .heading {
+      color: theme('colors.red');
+    }
   `
 
   return expect(
@@ -223,8 +300,10 @@ test('a missing nested theme property throws', () => {
 })
 
 test('a missing nested theme property with a close alternative throws with a suggestion', () => {
-  const input = `
-    .heading { color: theme('colors.yellw'); }
+  let input = css`
+    .heading {
+      color: theme('colors.yellw');
+    }
   `
 
   return expect(
@@ -241,8 +320,10 @@ test('a missing nested theme property with a close alternative throws with a sug
 })
 
 test('a path through a non-object throws', () => {
-  const input = `
-    .heading { color: theme('colors.yellow.100'); }
+  let input = css`
+    .heading {
+      color: theme('colors.yellow.100');
+    }
   `
 
   return expect(
@@ -259,8 +340,10 @@ test('a path through a non-object throws', () => {
 })
 
 test('a path which exists but is not a string throws', () => {
-  const input = `
-    .heading { color: theme('colors.yellow'); }
+  let input = css`
+    .heading {
+      color: theme('colors.yellow');
+    }
   `
 
   return expect(
@@ -275,8 +358,10 @@ test('a path which exists but is not a string throws', () => {
 })
 
 test('a path which exists but is invalid throws', () => {
-  const input = `
-    .heading { color: theme('colors'); }
+  let input = css`
+    .heading {
+      color: theme('colors');
+    }
   `
 
   return expect(
@@ -289,8 +374,10 @@ test('a path which exists but is invalid throws', () => {
 })
 
 test('a path which is an object throws with a suggested key', () => {
-  const input = `
-    .heading { color: theme('colors'); }
+  let input = css`
+    .heading {
+      color: theme('colors');
+    }
   `
 
   return expect(
@@ -307,12 +394,16 @@ test('a path which is an object throws with a suggested key', () => {
 })
 
 test('array values are joined by default', () => {
-  const input = `
-    .heading { font-family: theme('fontFamily.sans'); }
+  let input = css`
+    .heading {
+      font-family: theme('fontFamily.sans');
+    }
   `
 
-  const output = `
-    .heading { font-family: Inter, Helvetica, sans-serif; }
+  let output = css`
+    .heading {
+      font-family: Inter, Helvetica, sans-serif;
+    }
   `
 
   return run(input, {
@@ -328,14 +419,22 @@ test('array values are joined by default', () => {
 })
 
 test('font sizes are retrieved without default line-heights or letter-spacing', () => {
-  const input = `
-    .heading-1 { font-size: theme('fontSize.lg'); }
-    .heading-2 { font-size: theme('fontSize.xl'); }
+  let input = css`
+    .heading-1 {
+      font-size: theme('fontSize.lg');
+    }
+    .heading-2 {
+      font-size: theme('fontSize.xl');
+    }
   `
 
-  const output = `
-    .heading-1 { font-size: 20px; }
-    .heading-2 { font-size: 24px; }
+  let output = css`
+    .heading-1 {
+      font-size: 20px;
+    }
+    .heading-2 {
+      font-size: 24px;
+    }
   `
 
   return run(input, {
@@ -352,12 +451,16 @@ test('font sizes are retrieved without default line-heights or letter-spacing',
 })
 
 test('outlines are retrieved without default outline-offset', () => {
-  const input = `
-    .element { outline: theme('outline.black'); }
+  let input = css`
+    .element {
+      outline: theme('outline.black');
+    }
   `
 
-  const output = `
-    .element { outline: 2px dotted black; }
+  let output = css`
+    .element {
+      outline: 2px dotted black;
+    }
   `
 
   return run(input, {
@@ -373,12 +476,16 @@ test('outlines are retrieved without default outline-offset', () => {
 })
 
 test('font-family values are joined when an array', () => {
-  const input = `
-    .element { font-family: theme('fontFamily.sans'); }
+  let input = css`
+    .element {
+      font-family: theme('fontFamily.sans');
+    }
   `
 
-  const output = `
-    .element { font-family: Helvetica, Arial, sans-serif; }
+  let output = css`
+    .element {
+      font-family: Helvetica, Arial, sans-serif;
+    }
   `
 
   return run(input, {
@@ -394,12 +501,16 @@ test('font-family values are joined when an array', () => {
 })
 
 test('box-shadow values are joined when an array', () => {
-  const input = `
-    .element { box-shadow: theme('boxShadow.wtf'); }
+  let input = css`
+    .element {
+      box-shadow: theme('boxShadow.wtf');
+    }
   `
 
-  const output = `
-    .element { box-shadow: 0 0 2px black, 1px 2px 3px white; }
+  let output = css`
+    .element {
+      box-shadow: 0 0 2px black, 1px 2px 3px white;
+    }
   `
 
   return run(input, {
@@ -415,12 +526,16 @@ test('box-shadow values are joined when an array', () => {
 })
 
 test('transition-property values are joined when an array', () => {
-  const input = `
-    .element { transition-property: theme('transitionProperty.colors'); }
+  let input = css`
+    .element {
+      transition-property: theme('transitionProperty.colors');
+    }
   `
 
-  const output = `
-    .element { transition-property: color, fill; }
+  let output = css`
+    .element {
+      transition-property: color, fill;
+    }
   `
 
   return run(input, {
@@ -436,12 +551,16 @@ test('transition-property values are joined when an array', () => {
 })
 
 test('transition-duration values are joined when an array', () => {
-  const input = `
-    .element { transition-duration: theme('transitionDuration.lol'); }
+  let input = css`
+    .element {
+      transition-duration: theme('transitionDuration.lol');
+    }
   `
 
-  const output = `
-    .element { transition-duration: 1s, 2s; }
+  let output = css`
+    .element {
+      transition-duration: 1s, 2s;
+    }
   `
 
   return run(input, {
@@ -457,16 +576,18 @@ test('transition-duration values are joined when an array', () => {
 })
 
 test('basic screen function calls are expanded', () => {
-  const input = `
+  let input = css`
     @media screen(sm) {
-      .foo {}
+      .foo {
+      }
     }
   `
 
-  const output = `
-  @media (min-width: 600px) {
-    .foo {}
-  }
+  let output = css`
+    @media (min-width: 600px) {
+      .foo {
+      }
+    }
   `
 
   return run(input, {
@@ -478,16 +599,18 @@ test('basic screen function calls are expanded', () => {
 })
 
 test('screen function supports max-width screens', () => {
-  const input = `
+  let input = css`
     @media screen(sm) {
-      .foo {}
+      .foo {
+      }
     }
   `
 
-  const output = `
-  @media (max-width: 600px) {
-    .foo {}
-  }
+  let output = css`
+    @media (max-width: 600px) {
+      .foo {
+      }
+    }
   `
 
   return run(input, {
@@ -499,16 +622,18 @@ test('screen function supports max-width screens', () => {
 })
 
 test('screen function supports min-width screens', () => {
-  const input = `
+  let input = css`
     @media screen(sm) {
-      .foo {}
+      .foo {
+      }
     }
   `
 
-  const output = `
-  @media (min-width: 600px) {
-    .foo {}
-  }
+  let output = css`
+    @media (min-width: 600px) {
+      .foo {
+      }
+    }
   `
 
   return run(input, {
@@ -520,16 +645,18 @@ test('screen function supports min-width screens', () => {
 })
 
 test('screen function supports min-width and max-width screens', () => {
-  const input = `
+  let input = css`
     @media screen(sm) {
-      .foo {}
+      .foo {
+      }
     }
   `
 
-  const output = `
-  @media (min-width: 600px) and (max-width: 700px) {
-    .foo {}
-  }
+  let output = css`
+    @media (min-width: 600px) and (max-width: 700px) {
+      .foo {
+      }
+    }
   `
 
   return run(input, {
@@ -541,16 +668,18 @@ test('screen function supports min-width and max-width screens', () => {
 })
 
 test('screen function supports raw screens', () => {
-  const input = `
+  let input = css`
     @media screen(mono) {
-      .foo {}
+      .foo {
+      }
     }
   `
 
-  const output = `
-  @media monochrome {
-    .foo {}
-  }
+  let output = css`
+    @media monochrome {
+      .foo {
+      }
+    }
   `
 
   return run(input, {
@@ -562,16 +691,18 @@ test('screen function supports raw screens', () => {
 })
 
 test('screen arguments can be quoted', () => {
-  const input = `
+  let input = css`
     @media screen('sm') {
-      .foo {}
+      .foo {
+      }
     }
   `
 
-  const output = `
-  @media (min-width: 600px) {
-    .foo {}
-  }
+  let output = css`
+    @media (min-width: 600px) {
+      .foo {
+      }
+    }
   `
 
   return run(input, {
diff --git a/tests/normalize-screens.test.js b/tests/normalize-screens.test.js
new file mode 100644
index 00000000..1a5b4a88
--- /dev/null
+++ b/tests/normalize-screens.test.js
@@ -0,0 +1,98 @@
+import { normalizeScreens } from '../src/util/normalizeScreens'
+
+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 }] },
+  ])
+})
+
+it('should normalize an object with string values', () => {
+  let screens = {
+    a: '768px',
+    b: '1200px',
+  }
+
+  expect(normalizeScreens(screens)).toEqual([
+    { name: 'a', values: [{ min: '768px', max: undefined }] },
+    { name: 'b', values: [{ min: '1200px', max: undefined }] },
+  ])
+})
+
+it('should normalize an object with object values', () => {
+  let screens = {
+    a: { min: '768px' },
+    b: { max: '1200px' },
+  }
+
+  expect(normalizeScreens(screens)).toEqual([
+    { name: 'a', values: [{ min: '768px', max: undefined }] },
+    { name: 'b', values: [{ min: undefined, max: '1200px' }] },
+  ])
+})
+
+it('should normalize an object with multiple object values', () => {
+  let screens = {
+    a: [{ min: '768px' }, { max: '1200px' }],
+  }
+
+  expect(normalizeScreens(screens)).toEqual([
+    {
+      name: 'a',
+      values: [
+        { max: undefined, min: '768px', raw: undefined },
+        { max: '1200px', min: undefined, raw: undefined },
+      ],
+    },
+  ])
+})
+
+it('should normalize an object with object values (min-width normalized to width)', () => {
+  let screens = {
+    a: { 'min-width': '768px' },
+    b: { max: '1200px' },
+  }
+
+  expect(normalizeScreens(screens)).toEqual([
+    { name: 'a', values: [{ min: '768px', max: undefined }] },
+    { name: 'b', values: [{ min: undefined, max: '1200px' }] },
+  ])
+})
+
+it('should normalize a tuple with string values', () => {
+  let screens = [
+    ['a', '768px'],
+    ['b', '1200px'],
+  ]
+
+  expect(normalizeScreens(screens)).toEqual([
+    { name: 'a', values: [{ min: '768px', max: undefined }] },
+    { name: 'b', values: [{ min: '1200px', max: undefined }] },
+  ])
+})
+
+it('should normalize a tuple with object values', () => {
+  let screens = [
+    ['a', { min: '768px' }],
+    ['b', { max: '1200px' }],
+  ]
+
+  expect(normalizeScreens(screens)).toEqual([
+    { name: 'a', values: [{ min: '768px', max: undefined }] },
+    { name: 'b', values: [{ min: undefined, max: '1200px' }] },
+  ])
+})
+
+it('should normalize a tuple with object values (min-width normalized to width)', () => {
+  let screens = [
+    ['a', { 'min-width': '768px' }],
+    ['b', { max: '1200px' }],
+  ]
+
+  expect(normalizeScreens(screens)).toEqual([
+    { name: 'a', values: [{ min: '768px', max: undefined }] },
+    { name: 'b', values: [{ min: undefined, max: '1200px' }] },
+  ])
+})
diff --git a/tests/tailwind-screens.test.js b/tests/tailwind-screens.test.js
index 784bf40c..232ae011 100644
--- a/tests/tailwind-screens.test.js
+++ b/tests/tailwind-screens.test.js
@@ -65,3 +65,51 @@ test('`@tailwind screens` works as an alias for `@tailwind variants`', async ()
     `)
   })
 })
+
+test('screen names are in the correct order', () => {
+  // Using custom css function here, because otherwise with String.raw, we run
+  // into an issue with `\31 ` escapes. If we use `\31 ` then JS complains
+  // about strict mode. But `\\31 ` is not what it expected.
+  function css(templates) {
+    return templates.join('')
+  }
+
+  let config = {
+    content: [
+      {
+        raw: html`<div
+          class="768:font-light 1200:font-normal 1800:font-bold max-w-768 container"
+        ></div>`,
+      },
+    ],
+    theme: {
+      screens: [
+        [1800, { max: '1800px' }],
+        [1200, { max: '1200px' }],
+        [768, { max: '768px' }],
+      ],
+    },
+  }
+
+  return run('@tailwind utilities;', config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      @media (max-width: 1800px) {
+        .\\31 800\\:font-bold {
+          font-weight: 700;
+        }
+      }
+
+      @media (max-width: 1200px) {
+        .\\31 200\\:font-normal {
+          font-weight: 400;
+        }
+      }
+
+      @media (max-width: 768px) {
+        .\\37 68\\:font-light {
+          font-weight: 300;
+        }
+      }
+    `)
+  })
+})