Repository: tailwindlabs/tailwindcss
Pull request #5388 (2021-09-04 12:23:42)

Add `scroll-behavior` utilities
This PR adds utilities for the [`scroll-behavior`](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior) property.

I would recommend pushing people towards using this property responsibly in combination with the `motion-safe` variant. I don't believe we have to manually enable variants for plugins anymore but if we do let me know and I'll add `motion-safe` and `motion-reduce` to the defaults.


lukewarlow: I think tailwind generally only adds utilities for CSS with support in all three browsers. So if this does wait for Safari to unflag its support, here's it in plugin form: https://github.com/lukewarlow/tailwind-scroll-behavior

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45e11bea..23b83d11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,10 +19,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Add native `aspect-ratio` utilities ([#5359](https://github.com/tailwindlabs/tailwindcss/pull/5359))
 - Unify config callback helpers into single object ([#5382](https://github.com/tailwindlabs/tailwindcss/pull/5382))
 - Preserve original color format when adding opacity whenever possible ([#5154](https://github.com/tailwindlabs/tailwindcss/pull/5154))
-- Support `@apply` for classes defined outside of a `@layer` ([#5378](https://github.com/tailwindlabs/tailwindcss/pull/5378))
-- Add `accent-*` utilities for `accent-color` ([#5387](https://github.com/tailwindlabs/tailwindcss/pull/5387))
-- Add `will-change` utilities ([#5448](https://github.com/tailwindlabs/tailwindcss/pull/5448))
-- Add `text-indent` utilities ([#5449](https://github.com/tailwindlabs/tailwindcss/pull/5449))
+
+### Fixed
+
+- Fix defining colors as functions when color opacity plugins are disabled ([#5470](https://github.com/tailwindlabs/tailwindcss/pull/5470))
+
+## [2.2.15] - 2021-09-10
+
+### Fixed
+
+- Ensure using CLI without `-i` for input file continues to work even though deprecated ([#5464](https://github.com/tailwindlabs/tailwindcss/pull/5464))
+
+## [2.2.14] - 2021-09-08
+
+### Fixed
+
+- Only use `@defaults` in JIT, switch back to `clean-css` in case there's any meaningful differences in the output ([bf248cb](https://github.com/tailwindlabs/tailwindcss/commit/bf248cb0de889d48854fbdd26536f4a492556efd))
+
+## [2.2.13] - 2021-09-08
+
+### Fixed
+
+- Replace `clean-css` with `cssnano` for CDN builds to fix minified builds ([75cc3ca](https://github.com/tailwindlabs/tailwindcss/commit/75cc3ca305aedddc8a85f3df1a420fefad3fb5c4))
+
+## [2.2.12] - 2021-09-08
+
+### Fixed
+
+- Ensure that divide utilities inject a default border color ([#5438](https://github.com/tailwindlabs/tailwindcss/pull/5438))
+
+## [2.2.11] - 2021-09-07
+
+### Fixed
+
+- Rebundle to fix missing CLI peer dependencies
 
 ## [2.2.10] - 2021-09-06
 
@@ -47,13 +77,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
 - Improve accessibility of default link focus styles in Firefox ([#5082](https://github.com/tailwindlabs/tailwindcss/pull/5082))
-- JIT: Fix animation variants corrupting keyframes rules [#5223](https://github.com/tailwindlabs/tailwindcss/pull/5223)
-- JIT: Ignore escaped commas when splitting selectors to apply prefixes [#5239](https://github.com/tailwindlabs/tailwindcss/pull/5239/)
-- Nesting: Maintain PostCSS node sources when handling `@apply` [#5249](https://github.com/tailwindlabs/tailwindcss/pull/5249)
-- JIT: Fix support for animation lists [#5252](https://github.com/tailwindlabs/tailwindcss/pull/5252)
-- JIT: Fix arbitrary value support for `object-position` utilities [#5245](https://github.com/tailwindlabs/tailwindcss/pull/5245)
-- CLI: Abort watcher if stdin is closed to avoid zombie processes [#4997](https://github.com/tailwindlabs/tailwindcss/pull/4997)
-- JIT: Ignore arbitrary values with unbalanced brackets [#5293](https://github.com/tailwindlabs/tailwindcss/pull/5293)
+- JIT: Fix animation variants corrupting keyframes rules ([#5223](https://github.com/tailwindlabs/tailwindcss/pull/5223))
+- JIT: Ignore escaped commas when splitting selectors to apply prefixes ([#5239](https://github.com/tailwindlabs/tailwindcss/pull/5239/))
+- Nesting: Maintain PostCSS node sources when handling `@apply` ([#5249](https://github.com/tailwindlabs/tailwindcss/pull/5249))
+- JIT: Fix support for animation lists ([#5252](https://github.com/tailwindlabs/tailwindcss/pull/5252))
+- JIT: Fix arbitrary value support for `object-position` utilities ([#5245](https://github.com/tailwindlabs/tailwindcss/pull/5245))
+- CLI: Abort watcher if stdin is closed to avoid zombie processes ([#4997](https://github.com/tailwindlabs/tailwindcss/pull/4997))
+- JIT: Ignore arbitrary values with unbalanced brackets ([#5293](https://github.com/tailwindlabs/tailwindcss/pull/5293))
 
 ## [2.2.7] - 2021-07-23
 
@@ -1546,7 +1576,12 @@ No release notes
 
 - Everything!
 
-[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.10...HEAD
+[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.15...HEAD
+[2.2.15]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.14...v2.2.15
+[2.2.14]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.13...v2.2.14
+[2.2.13]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.12...v2.2.13
+[2.2.12]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.11...v2.2.12
+[2.2.11]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.10...v2.2.11
 [2.2.10]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.9...v2.2.10
 [2.2.9]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.8...v2.2.9
 [2.2.8]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.7...v2.2.8
diff --git a/colors.js b/colors.js
index f8797e77..e609e381 100644
--- a/colors.js
+++ b/colors.js
@@ -1,9 +1,13 @@
+const log = require('./lib/util/log').default
+
 let warned = []
 
 function warn({ version, from, to }) {
   if (!warned.includes(from)) {
-    console.log(`warn - As of Tailwind CSS ${version}, \`${from}\` has been renamed to \`${to}\`.`)
-    console.log('warn - Please update your color palette to eliminate this warning.')
+    log.warn([
+      `As of Tailwind CSS ${version}, \`${from}\` has been renamed to \`${to}\`.`,
+      'Please update your color palette to eliminate this warning.',
+    ])
     warned.push(from)
   }
 }
diff --git a/src/cli.js b/src/cli.js
index b3a37cd0..39b00e3e 100644
--- a/src/cli.js
+++ b/src/cli.js
@@ -351,6 +351,7 @@ async function build() {
   // TODO: Deprecate this in future versions
   if (!input && args['_'][1]) {
     console.error('[deprecation] Running tailwindcss without -i, please provide an input file.')
+    input = args['--input'] = args['_'][1]
   }
 
   if (input && !fs.existsSync((input = path.resolve(input)))) {
diff --git a/src/corePlugins.js b/src/corePlugins.js
index f9a9a97c..fab1b56c 100644
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -1014,7 +1014,11 @@ export let divideColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       divide: (value) => {
         if (!corePlugins('divideOpacity')) {
-          return { ['& > :not([hidden]) ~ :not([hidden])']: { 'border-color': value } }
+          return {
+            ['& > :not([hidden]) ~ :not([hidden])']: {
+              'border-color': toColorValue(value),
+            },
+          }
         }
 
         return {
@@ -1106,6 +1110,13 @@ export let overscrollBehavior = ({ addUtilities }) => {
   })
 }
 
+export let scrollBehavior = ({ addUtilities }) => {
+  addUtilities({
+    '.scroll-auto': { 'scroll-behavior': 'auto' },
+    '.scroll-smooth': { 'scroll-behavior': 'smooth' },
+  })
+}
+
 export let textOverflow = ({ addUtilities }) => {
   addUtilities({
     '.truncate': { overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' },
@@ -1174,9 +1185,10 @@ export let borderStyle = ({ addUtilities }) => {
 
 export let borderColor = ({ addBase, matchUtilities, theme, corePlugins }) => {
   if (!corePlugins('borderOpacity')) {
+    let value = theme('borderColor.DEFAULT', 'currentColor')
     addBase({
       '@defaults border-width': {
-        'border-color': theme('borderColor.DEFAULT', 'currentColor'),
+        'border-color': toColorValue(value),
       },
     })
   } else {
@@ -1193,7 +1205,9 @@ export let borderColor = ({ addBase, matchUtilities, theme, corePlugins }) => {
     {
       border: (value) => {
         if (!corePlugins('borderOpacity')) {
-          return { 'border-color': value }
+          return {
+            'border-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1213,7 +1227,9 @@ export let borderColor = ({ addBase, matchUtilities, theme, corePlugins }) => {
     {
       'border-t': (value) => {
         if (!corePlugins('borderOpacity')) {
-          return { 'border-top-color': value }
+          return {
+            'border-top-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1224,7 +1240,9 @@ export let borderColor = ({ addBase, matchUtilities, theme, corePlugins }) => {
       },
       'border-r': (value) => {
         if (!corePlugins('borderOpacity')) {
-          return { 'border-right-color': value }
+          return {
+            'border-right-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1235,7 +1253,9 @@ export let borderColor = ({ addBase, matchUtilities, theme, corePlugins }) => {
       },
       'border-b': (value) => {
         if (!corePlugins('borderOpacity')) {
-          return { 'border-bottom-color': value }
+          return {
+            'border-bottom-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1246,7 +1266,9 @@ export let borderColor = ({ addBase, matchUtilities, theme, corePlugins }) => {
       },
       'border-l': (value) => {
         if (!corePlugins('borderOpacity')) {
-          return { 'border-left-color': value }
+          return {
+            'border-left-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1272,7 +1294,9 @@ export let backgroundColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       bg: (value) => {
         if (!corePlugins('backgroundOpacity')) {
-          return { 'background-color': value }
+          return {
+            'background-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1539,7 +1563,7 @@ export let textColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       text: (value) => {
         if (!corePlugins('textOpacity')) {
-          return { color: value }
+          return { color: toColorValue(value) }
         }
 
         return withAlphaVariable({
@@ -1583,7 +1607,11 @@ export let placeholderColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       placeholder: (value) => {
         if (!corePlugins('placeholderOpacity')) {
-          return { '&::placeholder': { color: value } }
+          return {
+            '&::placeholder': {
+              color: toColorValue(value),
+            },
+          }
         }
 
         return {
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index 2762ce03..6f563efb 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -194,12 +194,9 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
       return getConfigValue(['corePlugins', path], true)
     },
-    variants: (path, defaultValue) => {
-      if (Array.isArray(tailwindConfig.variants)) {
-        return tailwindConfig.variants
-      }
-
-      return getConfigValue(['variants', path], defaultValue)
+    variants: () => {
+      // Preserved for backwards compatibility but not used in v3.0+
+      return []
     },
     addUserCss(userCss) {
       for (let [identifier, rule] of withIdentifiers(userCss)) {
diff --git a/src/util/createUtilityPlugin.js b/src/util/createUtilityPlugin.js
index 5d03df62..3fffc390 100644
--- a/src/util/createUtilityPlugin.js
+++ b/src/util/createUtilityPlugin.js
@@ -15,7 +15,7 @@ export default function createUtilityPlugin(
   { filterDefault = false, resolveArbitraryValue = asValue } = {}
 ) {
   let transformValue = transformThemeValue(themeKey)
-  return function ({ matchUtilities, variants, theme }) {
+  return function ({ matchUtilities, theme }) {
     for (let utilityVariation of utilityVariations) {
       let group = Array.isArray(utilityVariation[0]) ? utilityVariation : [utilityVariation]
 
@@ -38,7 +38,6 @@ export default function createUtilityPlugin(
                 Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
               )
             : theme(themeKey),
-          variants: variants(themeKey),
           type: asMap.get(resolveArbitraryValue) ?? 'any',
         }
       )
diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js
index 3fae7426..27132b48 100644
--- a/src/util/resolveConfig.js
+++ b/src/util/resolveConfig.js
@@ -6,16 +6,11 @@ import colors from '../../colors'
 import log from './log'
 import { defaults } from './defaults'
 import { toPath } from './toPath'
-import dlv from 'dlv'
 
 function isFunction(input) {
   return typeof input === 'function'
 }
 
-function uniq(input) {
-  return Array.from(new Set(input))
-}
-
 function isObject(input) {
   return typeof input === 'object' && input !== null
 }
@@ -186,90 +181,6 @@ function extractPluginConfigs(configs) {
   return allConfigs
 }
 
-function mergeVariants(variants) {
-  const mergedVariants = variants.reduce((resolved, variants) => {
-    Object.entries(variants || {}).forEach(([plugin, pluginVariants]) => {
-      if (isFunction(pluginVariants)) {
-        resolved[plugin] = pluginVariants({
-          variants(path) {
-            return dlv(resolved, path, [])
-          },
-          before(toInsert, variant, existingPluginVariants = resolved?.[plugin] ?? []) {
-            if (variant === undefined) {
-              return [...toInsert, ...existingPluginVariants]
-            }
-
-            const index = existingPluginVariants.indexOf(variant)
-
-            if (index === -1) {
-              return [...existingPluginVariants, ...toInsert]
-            }
-
-            return [
-              ...existingPluginVariants.slice(0, index),
-              ...toInsert,
-              ...existingPluginVariants.slice(index),
-            ]
-          },
-          after(toInsert, variant, existingPluginVariants = resolved?.[plugin] ?? []) {
-            if (variant === undefined) {
-              return [...existingPluginVariants, ...toInsert]
-            }
-
-            const index = existingPluginVariants.indexOf(variant)
-
-            if (index === -1) {
-              return [...toInsert, ...existingPluginVariants]
-            }
-
-            return [
-              ...existingPluginVariants.slice(0, index + 1),
-              ...toInsert,
-              ...existingPluginVariants.slice(index + 1),
-            ]
-          },
-          without(toRemove, existingPluginVariants = resolved?.[plugin] ?? []) {
-            return existingPluginVariants.filter((v) => !toRemove.includes(v))
-          },
-        })
-      } else {
-        resolved[plugin] = pluginVariants
-      }
-    })
-
-    return resolved
-  }, {})
-
-  return {
-    ...mergedVariants,
-    extend: collectExtends(variants),
-  }
-}
-
-function mergeVariantExtensions({ extend, ...variants }, variantOrder) {
-  return mergeWith(variants, extend, (variantsValue, extensions) => {
-    const merged = uniq([...(variantsValue || []), ...extensions].flat())
-
-    if (extensions.flat().length === 0) {
-      return merged
-    }
-
-    return merged.sort((a, z) => variantOrder.indexOf(a) - variantOrder.indexOf(z))
-  })
-}
-
-function resolveVariants([firstConfig, ...variantConfigs], variantOrder) {
-  // Global variants configuration like `variants: ['hover', 'focus']`
-  if (Array.isArray(firstConfig)) {
-    return firstConfig
-  }
-
-  return mergeVariantExtensions(
-    mergeVariants([firstConfig, ...variantConfigs].reverse()),
-    variantOrder
-  )
-}
-
 function resolveCorePlugins(corePluginConfigs) {
   const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => {
     if (isFunction(corePluginConfig)) {
@@ -299,7 +210,6 @@ export default function resolveConfig(configs) {
       variantOrder: defaultConfig.variantOrder,
     },
   ]
-  let { variantOrder } = allConfigs.find((c) => c.variantOrder)
 
   return normalizeConfig(
     defaults(
@@ -307,10 +217,6 @@ export default function resolveConfig(configs) {
         theme: resolveFunctionKeys(
           mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
         ),
-        variants: resolveVariants(
-          allConfigs.map((c) => c?.variants ?? {}),
-          variantOrder
-        ),
         corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
         plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
       },


Test patch:
diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js
index 1256ff8b..4d52cd5f 100644
--- a/tests/custom-plugins.test.js
+++ b/tests/custom-plugins.test.js
@@ -1217,15 +1217,14 @@ test('plugins can be created using the `createPlugin` function', () => {
     },
     plugins: [
       createPlugin(
-        function ({ addUtilities, theme, variants }) {
+        function ({ addUtilities, theme }) {
           addUtilities(
             Object.fromEntries(
               Object.entries(theme('testPlugin')).map(([k, v]) => [
                 `.test-${k}`,
                 { testProperty: v },
               ])
-            ),
-            variants('testPlugin')
+            )
           )
         },
         {
@@ -1404,9 +1403,6 @@ test('plugins created using `createPlugin.withOptions` do not need to be invoked
             lg: '3rem',
           },
         },
-        variants: {
-          testPlugin: ['responsive', 'hover'],
-        },
       }
     }
   )
diff --git a/tests/opacity.test.css b/tests/opacity.test.css
deleted file mode 100644
index c665bb7b..00000000
--- a/tests/opacity.test.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.divide-black > :not([hidden]) ~ :not([hidden]) {
-  border-color: #000;
-}
-.border-black {
-  border-color: #000;
-}
-.bg-black {
-  background-color: #000;
-}
-.text-black {
-  color: #000;
-}
-.placeholder-black::placeholder {
-  color: #000;
-}
diff --git a/tests/opacity.test.html b/tests/opacity.test.html
deleted file mode 100644
index 6942d771..00000000
--- a/tests/opacity.test.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" href="/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Title</title>
-    <link rel="stylesheet" href="./tailwind.css" />
-  </head>
-  <body>
-    <div class="divide-black"></div>
-    <div class="border-black"></div>
-    <div class="bg-black"></div>
-    <div class="text-black"></div>
-    <div class="placeholder-black"></div>
-  </body>
-</html>
diff --git a/tests/opacity.test.js b/tests/opacity.test.js
index 9fde486b..6e270eb1 100644
--- a/tests/opacity.test.js
+++ b/tests/opacity.test.js
@@ -1,12 +1,19 @@
-import fs from 'fs'
-import path from 'path'
-
-import { run } from './util/run'
+import { run, html, css } from './util/run'
 
 test('opacity', () => {
   let config = {
     darkMode: 'class',
-    content: [path.resolve(__dirname, './opacity.test.html')],
+    content: [
+      {
+        raw: html`
+          <div class="divide-black"></div>
+          <div class="border-black"></div>
+          <div class="bg-black"></div>
+          <div class="text-black"></div>
+          <div class="placeholder-black"></div>
+        `,
+      },
+    ],
     corePlugins: {
       backgroundOpacity: false,
       borderOpacity: false,
@@ -17,9 +24,74 @@ test('opacity', () => {
   }
 
   return run('@tailwind utilities', config).then((result) => {
-    let expectedPath = path.resolve(__dirname, './opacity.test.css')
-    let expected = fs.readFileSync(expectedPath, 'utf8')
+    expect(result.css).toMatchCss(css`
+      .divide-black > :not([hidden]) ~ :not([hidden]) {
+        border-color: #000;
+      }
+      .border-black {
+        border-color: #000;
+      }
+      .bg-black {
+        background-color: #000;
+      }
+      .text-black {
+        color: #000;
+      }
+      .placeholder-black::placeholder {
+        color: #000;
+      }
+    `)
+  })
+})
+
+test('colors defined as functions work when opacity plugins are disabled', () => {
+  let config = {
+    darkMode: 'class',
+    content: [
+      {
+        raw: html`
+          <div class="divide-primary"></div>
+          <div class="border-primary"></div>
+          <div class="bg-primary"></div>
+          <div class="text-primary"></div>
+          <div class="placeholder-primary"></div>
+        `,
+      },
+    ],
+    theme: {
+      colors: {
+        primary: ({ opacityValue }) =>
+          opacityValue === undefined
+            ? 'rgb(var(--color-primary))'
+            : `rgb(var(--color-primary) / ${opacityValue})`,
+      },
+    },
+    corePlugins: {
+      backgroundOpacity: false,
+      borderOpacity: false,
+      divideOpacity: false,
+      placeholderOpacity: false,
+      textOpacity: false,
+    },
+  }
 
-    expect(result.css).toMatchCss(expected)
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .divide-primary > :not([hidden]) ~ :not([hidden]) {
+        border-color: rgb(var(--color-primary));
+      }
+      .border-primary {
+        border-color: rgb(var(--color-primary));
+      }
+      .bg-primary {
+        background-color: rgb(var(--color-primary));
+      }
+      .text-primary {
+        color: rgb(var(--color-primary));
+      }
+      .placeholder-primary::placeholder {
+        color: rgb(var(--color-primary));
+      }
+    `)
   })
 })
diff --git a/tests/raw-content.test.css b/tests/raw-content.test.css
index 0c466140..75ea33b8 100644
--- a/tests/raw-content.test.css
+++ b/tests/raw-content.test.css
@@ -364,6 +364,9 @@
 .overscroll-contain {
   overscroll-behavior: contain;
 }
+.scroll-smooth {
+  scroll-behavior: smooth;
+}
 .truncate {
   overflow: hidden;
   text-overflow: ellipsis;
diff --git a/tests/raw-content.test.html b/tests/raw-content.test.html
index 1e97f5fd..2cb65ed4 100644
--- a/tests/raw-content.test.html
+++ b/tests/raw-content.test.html
@@ -96,6 +96,7 @@
     <div class="outline-none outline-black"></div>
     <div class="overflow-hidden"></div>
     <div class="overscroll-contain"></div>
+    <div class="scroll-smooth"></div>
     <div class="p-4 py-2 px-3 pt-1 pr-2 pb-3 pl-4"></div>
     <div class="place-content-start"></div>
     <div class="placeholder-green-300"></div>
diff --git a/tests/resolveConfig.test.js b/tests/resolveConfig.test.js
index b5b6147e..dba44d32 100644
--- a/tests/resolveConfig.test.js
+++ b/tests/resolveConfig.test.js
@@ -15,11 +15,6 @@ test('prefix key overrides default prefix', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -33,11 +28,6 @@ test('prefix key overrides default prefix', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -55,11 +45,6 @@ test('important key overrides default important', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -73,11 +58,6 @@ test('important key overrides default important', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -95,11 +75,6 @@ test('important (selector) key overrides default important', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -113,11 +88,6 @@ test('important (selector) key overrides default important', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -135,11 +105,6 @@ test('separator key overrides default separator', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -153,11 +118,6 @@ test('separator key overrides default separator', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -192,11 +152,6 @@ test('theme key is merged instead of replaced', () => {
         lg: '1000px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -221,11 +176,6 @@ test('theme key is merged instead of replaced', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -278,143 +228,6 @@ test('theme key is deeply merged instead of replaced', () => {
   })
 })
 
-test('variants key is merged instead of replaced', () => {
-  const userConfig = {
-    variants: {
-      backgroundAttachment: [],
-      borderColors: ['responsive', 'hover', 'focus', 'active'],
-    },
-  }
-
-  const defaultConfig = {
-    prefix: '-',
-    important: false,
-    separator: ':',
-    theme: {
-      colors: {
-        'grey-darker': '#606f7b',
-        'grey-dark': '#8795a1',
-        grey: '#b8c2cc',
-        'grey-light': '#dae1e7',
-        'grey-lighter': '#f1f5f8',
-      },
-      fonts: {
-        sans: ['system-ui', 'BlinkMacSystemFont', '-apple-system', 'Roboto', 'sans-serif'],
-        serif: ['Constantia', 'Lucida Bright', 'Georgia', 'serif'],
-      },
-      screens: {
-        sm: '500px',
-        md: '750px',
-        lg: '1000px',
-      },
-    },
-    variants: {
-      appearance: ['responsive'],
-      backgroundAttachment: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-      borderRadius: ['responsive'],
-    },
-  }
-
-  const result = resolveConfig([userConfig, defaultConfig])
-
-  expect(result).toMatchObject({
-    prefix: '-',
-    important: false,
-    separator: ':',
-    theme: {
-      colors: {
-        'grey-darker': '#606f7b',
-        'grey-dark': '#8795a1',
-        grey: '#b8c2cc',
-        'grey-light': '#dae1e7',
-        'grey-lighter': '#f1f5f8',
-      },
-      fonts: {
-        sans: ['system-ui', 'BlinkMacSystemFont', '-apple-system', 'Roboto', 'sans-serif'],
-        serif: ['Constantia', 'Lucida Bright', 'Georgia', 'serif'],
-      },
-      screens: {
-        sm: '500px',
-        md: '750px',
-        lg: '1000px',
-      },
-    },
-    variants: {
-      appearance: ['responsive'],
-      backgroundAttachment: [],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus', 'active'],
-      borderRadius: ['responsive'],
-    },
-  })
-})
-
-test('a global variants list replaces the default', () => {
-  const userConfig = {
-    variants: ['responsive', 'hover', 'focus', 'active'],
-  }
-
-  const defaultConfig = {
-    prefix: '-',
-    important: false,
-    separator: ':',
-    theme: {
-      colors: {
-        'grey-darker': '#606f7b',
-        'grey-dark': '#8795a1',
-        grey: '#b8c2cc',
-        'grey-light': '#dae1e7',
-        'grey-lighter': '#f1f5f8',
-      },
-      fonts: {
-        sans: ['system-ui', 'BlinkMacSystemFont', '-apple-system', 'Roboto', 'sans-serif'],
-        serif: ['Constantia', 'Lucida Bright', 'Georgia', 'serif'],
-      },
-      screens: {
-        sm: '500px',
-        md: '750px',
-        lg: '1000px',
-      },
-    },
-    variants: {
-      appearance: ['responsive'],
-      backgroundAttachment: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-      borderRadius: ['responsive'],
-    },
-  }
-
-  const result = resolveConfig([userConfig, defaultConfig])
-
-  expect(result).toMatchObject({
-    prefix: '-',
-    important: false,
-    separator: ':',
-    theme: {
-      colors: {
-        'grey-darker': '#606f7b',
-        'grey-dark': '#8795a1',
-        grey: '#b8c2cc',
-        'grey-light': '#dae1e7',
-        'grey-lighter': '#f1f5f8',
-      },
-      fonts: {
-        sans: ['system-ui', 'BlinkMacSystemFont', '-apple-system', 'Roboto', 'sans-serif'],
-        serif: ['Constantia', 'Lucida Bright', 'Georgia', 'serif'],
-      },
-      screens: {
-        sm: '500px',
-        md: '750px',
-        lg: '1000px',
-      },
-    },
-    variants: ['responsive', 'hover', 'focus', 'active'],
-  })
-})
-
 test('missing top level keys are pulled from the default config', () => {
   const userConfig = {}
 
@@ -428,11 +241,6 @@ test('missing top level keys are pulled from the default config', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -447,11 +255,6 @@ test('missing top level keys are pulled from the default config', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -479,10 +282,6 @@ test('functions in the default theme section are lazily evaluated', () => {
       backgroundColors: (theme) => theme('colors'),
       textColors: (theme) => theme('colors'),
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-      textColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -508,10 +307,6 @@ test('functions in the default theme section are lazily evaluated', () => {
         blue: 'blue',
       },
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-      textColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -547,10 +342,6 @@ test('functions in the user theme section are lazily evaluated', () => {
       backgroundColors: ({ colors }) => colors,
       textColors: ({ colors }) => colors,
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-      textColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -578,10 +369,6 @@ test('functions in the user theme section are lazily evaluated', () => {
         customText: '#facade',
       },
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-      textColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -617,10 +404,6 @@ test('theme values in the extend section extend the existing theme', () => {
       },
       backgroundColors: (theme) => theme('colors'),
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-      opacity: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -649,10 +432,6 @@ test('theme values in the extend section extend the existing theme', () => {
         customBackground: '#bada55',
       },
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-      opacity: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -700,11 +479,6 @@ test('theme values in the extend section extend the user theme', () => {
         4: '1rem',
       },
     },
-    variants: {
-      opacity: ['responsive', 'hover', 'focus'],
-      height: ['responsive'],
-      width: ['responsive'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -738,11 +512,6 @@ test('theme values in the extend section extend the user theme', () => {
         4: '1rem',
       },
     },
-    variants: {
-      opacity: ['responsive', 'hover', 'focus'],
-      height: ['responsive'],
-      width: ['responsive'],
-    },
   })
 })
 
@@ -774,9 +543,6 @@ test('theme values in the extend section can extend values that are depended on
       },
       backgroundColors: (theme) => theme('colors'),
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -804,9 +570,6 @@ test('theme values in the extend section can extend values that are depended on
         customBackground: '#bada55',
       },
     },
-    variants: {
-      backgroundColors: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -832,9 +595,6 @@ test('theme values in the extend section are not deeply merged when they are sim
         mono: ['Menlo', 'Courier New', 'monospace'],
       },
     },
-    variants: {
-      fonts: ['responsive'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -850,9 +610,6 @@ test('theme values in the extend section are not deeply merged when they are sim
         mono: ['Menlo', 'Courier New', 'monospace'],
       },
     },
-    variants: {
-      fonts: ['responsive'],
-    },
   })
 })
 
@@ -892,7 +649,6 @@ test('theme values in the extend section are deeply merged, when they are arrays
         },
       },
     },
-    variants: {},
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -922,7 +678,6 @@ test('theme values in the extend section are deeply merged, when they are arrays
         },
       },
     },
-    variants: {},
   })
 })
 
@@ -952,9 +707,6 @@ test('the theme function can use a default value if the key is missing', () => {
         ...theme('colors'),
       }),
     },
-    variants: {
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -976,9 +728,6 @@ test('the theme function can use a default value if the key is missing', () => {
         blue: 'blue',
       },
     },
-    variants: {
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -1008,10 +757,6 @@ test('the theme function can resolve function values', () => {
         blue: 'blue',
       },
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1047,10 +792,6 @@ test('the theme function can resolve function values', () => {
         blue: 'blue',
       },
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -1076,10 +817,6 @@ test('the theme function can resolve deep function values', () => {
         '1/3': '33.33333%',
       }),
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1100,10 +837,6 @@ test('the theme function can resolve deep function values', () => {
         '1/3': '33.33333%',
       },
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -1142,9 +875,6 @@ test('theme values in the extend section are lazily evaluated', () => {
         ...theme('colors'),
       }),
     },
-    variants: {
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1170,9 +900,6 @@ test('theme values in the extend section are lazily evaluated', () => {
         blue: 'blue',
       },
     },
-    variants: {
-      borderColor: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -1209,7 +936,6 @@ test('lazily evaluated values have access to the config utils', () => {
         ...negative(theme('spacing')),
       }),
     },
-    variants: {},
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1266,7 +992,6 @@ test('lazily evaluated values have access to the config utils', () => {
         4: '4px',
       },
     },
-    variants: {},
   })
 })
 
@@ -1279,9 +1004,6 @@ test('the original theme is not mutated', () => {
         },
       },
     },
-    variants: {
-      borderColor: ['responsive', 'hover'],
-    },
   }
 
   const defaultConfig = {
@@ -1295,9 +1017,6 @@ test('the original theme is not mutated', () => {
         yellow: 'yellow',
       },
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-    },
   }
 
   resolveConfig([userConfig, defaultConfig])
@@ -1310,9 +1029,6 @@ test('the original theme is not mutated', () => {
         },
       },
     },
-    variants: {
-      borderColor: ['responsive', 'hover'],
-    },
   })
 })
 
@@ -1343,7 +1059,6 @@ test('custom properties are multiplied by -1 for negative values', () => {
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1460,9 +1175,6 @@ test('more than two config objects can be resolved', () => {
       },
       backgroundColor: (theme) => theme('colors'),
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([firstConfig, secondConfig, thirdConfig, defaultConfig])
@@ -1503,9 +1215,6 @@ test('more than two config objects can be resolved', () => {
         lime: 'lime',
       },
     },
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-    },
   })
 })
 
@@ -1529,11 +1238,6 @@ test('plugin config modifications are applied', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1547,11 +1251,6 @@ test('plugin config modifications are applied', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
     plugins: userConfig.plugins,
   })
 })
@@ -1577,11 +1276,6 @@ test('user config takes precedence over plugin config modifications', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1595,11 +1289,6 @@ test('user config takes precedence over plugin config modifications', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
     plugins: userConfig.plugins,
   })
 })
@@ -1637,11 +1326,6 @@ test('plugin config can register plugins that also have config', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1655,11 +1339,6 @@ test('plugin config can register plugins that also have config', () => {
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
     plugins: userConfig.plugins,
   })
 })
@@ -1692,11 +1371,6 @@ test('plugin configs take precedence over plugin configs registered by that plug
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1710,11 +1384,6 @@ test('plugin configs take precedence over plugin configs registered by that plug
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
     plugins: userConfig.plugins,
   })
 })
@@ -1757,11 +1426,6 @@ test('plugin theme extensions are added even if user overrides top-level theme c
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1780,11 +1444,6 @@ test('plugin theme extensions are added even if user overrides top-level theme c
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
     plugins: userConfig.plugins,
   })
 })
@@ -1828,11 +1487,6 @@ test('user theme extensions take precedence over plugin theme extensions with th
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -1852,56 +1506,10 @@ test('user theme extensions take precedence over plugin theme extensions with th
         mobile: '400px',
       },
     },
-    variants: {
-      appearance: ['responsive'],
-      borderCollapse: [],
-      borderColors: ['responsive', 'hover', 'focus'],
-    },
     plugins: userConfig.plugins,
   })
 })
 
-test('variants can be extended', () => {
-  const userConfig = {
-    variants: {
-      borderColor: ({ after }) => after(['group-focus'], 'hover'),
-      extend: {
-        backgroundColor: ['active', 'disabled', 'group-hover'],
-      },
-    },
-  }
-
-  const otherConfig = {
-    variants: {
-      extend: {
-        textColor: ['hover', 'focus-within'],
-      },
-    },
-  }
-
-  const defaultConfig = {
-    prefix: '',
-    important: false,
-    separator: ':',
-    theme: {},
-    variants: {
-      borderColor: ['hover', 'focus'],
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      textColor: ['responsive', 'focus'],
-    },
-  }
-
-  const result = resolveConfig([userConfig, otherConfig, defaultConfig])
-
-  expect(result).toMatchObject({
-    variants: {
-      borderColor: ['hover', 'group-focus', 'focus'],
-      backgroundColor: ['responsive', 'group-hover', 'hover', 'focus', 'active', 'disabled'],
-      textColor: ['responsive', 'focus-within', 'hover', 'focus'],
-    },
-  })
-})
-
 test('extensions are applied in the right order', () => {
   const userConfig = {
     theme: {
@@ -1966,153 +1574,6 @@ test('extensions are applied in the right order', () => {
   })
 })
 
-test('variant sort order can be customized', () => {
-  const userConfig = {
-    variantOrder: [
-      'disabled',
-      'focus',
-      'group-hover',
-      'focus-within',
-      'active',
-      'hover',
-      'responsive',
-    ],
-    variants: {
-      borderColor: ({ after }) => after(['group-focus'], 'hover'),
-      extend: {
-        backgroundColor: ['active', 'disabled', 'group-hover'],
-      },
-    },
-  }
-
-  const otherConfig = {
-    variants: {
-      extend: {
-        textColor: ['hover', 'focus-within'],
-      },
-    },
-  }
-
-  const defaultConfig = {
-    prefix: '',
-    important: false,
-    separator: ':',
-    theme: {},
-    variants: {
-      borderColor: ['hover', 'focus'],
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      textColor: ['responsive', 'focus'],
-    },
-  }
-
-  const result = resolveConfig([userConfig, otherConfig, defaultConfig])
-
-  expect(result).toMatchObject({
-    variants: {
-      borderColor: ['hover', 'group-focus', 'focus'],
-      backgroundColor: ['disabled', 'focus', 'group-hover', 'active', 'hover', 'responsive'],
-      textColor: ['focus', 'focus-within', 'hover', 'responsive'],
-    },
-  })
-})
-
-test('custom variants go to the beginning by default when sort is applied', () => {
-  const userConfig = {
-    variants: {
-      extend: {
-        backgroundColor: ['active', 'custom-variant-1', 'group-hover', 'custom-variant-2'],
-      },
-    },
-  }
-
-  const defaultConfig = {
-    prefix: '',
-    important: false,
-    separator: ':',
-    theme: {},
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-    },
-  }
-
-  const result = resolveConfig([userConfig, defaultConfig])
-
-  expect(result).toMatchObject({
-    variants: {
-      backgroundColor: [
-        'responsive',
-        'custom-variant-1',
-        'custom-variant-2',
-        'group-hover',
-        'hover',
-        'focus',
-        'active',
-      ],
-    },
-  })
-})
-
-test('variants can be defined as a function', () => {
-  const userConfig = {
-    variants: {
-      backgroundColor: ({ variants }) => [...variants('backgroundColor'), 'disabled'],
-      padding: ({ before }) => before(['active']),
-      float: ({ before }) => before(['disabled'], 'focus'),
-      margin: ({ before }) => before(['hover'], 'focus'),
-      borderWidth: ({ after }) => after(['active']),
-      backgroundImage: ({ after }) => after(['disabled'], 'hover'),
-      opacity: ({ after }) => after(['hover'], 'focus'),
-      rotate: ({ without }) => without(['hover']),
-      cursor: ({ before, after, without }) =>
-        without(['responsive'], before(['checked'], 'hover', after(['hover'], 'focus'))),
-    },
-  }
-
-  const otherConfig = {
-    variants: {
-      backgroundColor: ({ variants }) => [...variants('backgroundColor'), 'active'],
-    },
-  }
-
-  const defaultConfig = {
-    prefix: '',
-    important: false,
-    separator: ':',
-    theme: {},
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus'],
-      padding: ['responsive', 'focus'],
-      float: ['responsive', 'hover', 'focus'],
-      margin: ['responsive'],
-      borderWidth: ['responsive', 'focus'],
-      backgroundImage: ['responsive', 'hover', 'focus'],
-      opacity: ['responsive'],
-      rotate: ['responsive', 'hover', 'focus'],
-      cursor: ['responsive', 'focus'],
-    },
-  }
-
-  const result = resolveConfig([userConfig, otherConfig, defaultConfig])
-
-  expect(result).toMatchObject({
-    prefix: '',
-    important: false,
-    separator: ':',
-    theme: {},
-    variants: {
-      backgroundColor: ['responsive', 'hover', 'focus', 'active', 'disabled'],
-      padding: ['active', 'responsive', 'focus'],
-      float: ['responsive', 'hover', 'disabled', 'focus'],
-      margin: ['responsive', 'hover'],
-      borderWidth: ['responsive', 'focus', 'active'],
-      backgroundImage: ['responsive', 'hover', 'disabled', 'focus'],
-      opacity: ['hover', 'responsive'],
-      rotate: ['responsive', 'focus'],
-      cursor: ['focus', 'checked', 'hover'],
-    },
-  })
-})
-
 test('core plugin configuration builds on the default list when starting with an empty object', () => {
   const userConfig = {
     corePlugins: { display: false },
@@ -2123,7 +1584,6 @@ test('core plugin configuration builds on the default list when starting with an
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
     corePlugins: {},
   }
 
@@ -2134,7 +1594,6 @@ test('core plugin configuration builds on the default list when starting with an
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
     corePlugins: corePluginList.filter((c) => c !== 'display'),
   })
 })
@@ -2150,7 +1609,6 @@ test('core plugins that are disabled by default can be enabled', () => {
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
     corePlugins: { display: false },
   }
 
@@ -2174,7 +1632,6 @@ test('core plugin configurations stack', () => {
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
     corePlugins: ['float', 'display', 'padding'],
   }
 
@@ -2185,7 +1642,6 @@ test('core plugin configurations stack', () => {
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
     corePlugins: ['float', 'padding', 'margin'],
   })
 })
@@ -2205,7 +1661,6 @@ test('plugins are merged', () => {
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
   }
 
   const result = resolveConfig([userConfig, otherConfig, defaultConfig])
@@ -2215,7 +1670,6 @@ test('plugins are merged', () => {
     important: false,
     separator: ':',
     theme: {},
-    variants: {},
     plugins: ['1', '2', '3'],
   })
 })
@@ -2253,7 +1707,6 @@ test('all helpers can be destructured from the first function argument', () => {
         4: '4px',
       },
     },
-    variants: {},
   }
 
   const result = resolveConfig([userConfig, defaultConfig])
@@ -2275,6 +1728,5 @@ test('all helpers can be destructured from the first function argument', () => {
         'screen-md': '768px',
       },
     },
-    variants: {},
   })
 })
diff --git a/tests/util/invokePlugin.js b/tests/util/invokePlugin.js
deleted file mode 100644
index 226d6470..00000000
--- a/tests/util/invokePlugin.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import dlv from 'dlv'
-import escapeClassName from '../src/util/escapeClassName'
-
-export default function (plugin, config) {
-  const addedUtilities = []
-
-  const getConfigValue = (path, defaultValue) => dlv(config, path, defaultValue)
-  const pluginApi = {
-    config: getConfigValue,
-    e: escapeClassName,
-    theme: (path, defaultValue) => getConfigValue(`theme.${path}`, defaultValue),
-    variants: (path, defaultValue) => {
-      if (Array.isArray(config.variants)) {
-        return config.variants
-      }
-
-      return getConfigValue(`variants.${path}`, defaultValue)
-    },
-    addUtilities(utilities, variants) {
-      addedUtilities.push([utilities, variants])
-    },
-    corePlugins(corePlugin) {
-      if (config.corePlugins === undefined) {
-        return false
-      }
-
-      if (Array.isArray(config.corePlugins)) {
-        return config.corePlugins.includes(corePlugin)
-      }
-
-      return config.corePlugins[corePlugin] !== false
-    },
-  }
-
-  plugin(pluginApi)
-
-  return {
-    utilities: addedUtilities.map(([utilities, variants]) => [
-      Object.assign({}, ...(Array.isArray(utilities) ? utilities : [utilities])),
-      variants,
-    ]),
-  }
-}
diff --git a/tests/util/produce.js b/tests/util/produce.js
deleted file mode 100644
index 1949a1ad..00000000
--- a/tests/util/produce.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// Full credit goes to: https://github.com/purplestone/exhaust
-// However, it is modified so that it is a bit more modern
-export function produce(blueprint) {
-  let groups = []
-
-  // Call the blueprint once so that we can collect all the possible values we
-  // spit out in the callback function.
-  blueprint((...args) => {
-    if (args.length <= 0) throw new Error('Blueprint callback must have at least a single value')
-    groups.push(args)
-  })
-
-  // Calculate how many combinations there are
-  let iterations = groups.reduce((total, current) => total * current.length, 1)
-
-  // Calculate all the combinations possible
-  let zippedGroups = []
-  let currentIteration = iterations
-  groups.forEach((a) => {
-    let n = a.length
-    currentIteration = currentIteration / n
-    let iS = -1
-    let aS = []
-
-    for (let i = 0; i < iterations; i++) {
-      if (!(i % currentIteration)) iS++
-      aS.push(a[iS % n])
-    }
-    zippedGroups.push(aS)
-  })
-
-  // Transpose the matrix, so that we can get the correct rows/columns structure
-  // again.
-  zippedGroups = zippedGroups[0].map((_, i) => zippedGroups.map((o) => o[i]))
-
-  // Call the blueprint again, but now give the inner function a single value
-  // every time so that we can build up the final result with single values.
-  return zippedGroups.map((group) => {
-    let i = 0
-    return blueprint(() => group[i++])
-  })
-}