Repository: tailwindlabs/tailwindcss
Pull request #4936 (2021-07-09 17:02:18)

Add variant for `::file-selector-button` pseudo element
This PR adds a variant for the [`::file-selector-button`](https://developer.mozilla.org/en-US/docs/Web/CSS/::file-selector-button)  pseudo element, which is supported by the latest versions of all three engines.

I think I've added to the relevant tests but feel free to point out any others that are required.


mainej: +1 I'd love to have this too. Would address https://github.com/tailwindlabs/tailwindcss-forms/issues/25

Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 35cd0c10..cfd8d5c2 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -27,7 +27,7 @@ npm run style -- --fix
 You can run the test suite using the following commands:
 
 ```sh
-npm test
+npm run babelify && npm test
 ```
 
 Please ensure that the tests are passing when submitting a pull request. If you're adding new features to Tailwind, please include tests.
diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml
deleted file mode 100644
index 769d1649..00000000
--- a/.github/ISSUE_TEMPLATE/1.bug_report.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Bug Report
-description: Create a bug report for Tailwind CSS.
-labels: []
-body:
-  - type: input
-    attributes:
-      label: What version of Tailwind CSS are you using?
-      description: 'For example: v2.0.4'
-    validations:
-      required: true
-  - type: input
-    attributes:
-      label: What build tool (or framework if it abstracts the build tool) are you using?
-      description: 'For example: postcss-cli 8.3.1, Next.js 10.0.9, webpack 5.28.0'
-    validations:
-      required: true  
-  - type: input
-    attributes:
-      label: What version of Node.js are you using?
-      description: 'For example: v12.0.0'
-    validations:
-      required: true
-  - type: input
-    attributes:
-      label: What browser are you using?
-      description: 'For example: Chrome, Safari, or N/A'
-    validations:
-      required: true
-  - type: input
-    attributes:
-      label: What operating system are you using?
-      description: 'For example: macOS, Windows'
-    validations:
-      required: true
-  - type: input
-    attributes:
-      label: Reproduction repository
-      description: A public GitHub repo that includes a minimal reproduction of the bug. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn't matter if your real project is private/confidential, since we want a link to a separate, isolated reproduction anyways. Unfortunately we can't provide support without a reproduction, and your issue will be closed and locked with no comment if this is not provided.
-    validations:
-      required: true
-  - type: textarea
-    attributes:
-      label: Describe your issue
-      description: Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 93e5f82d..3e0df6d6 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,14 +1,14 @@
 blank_issues_enabled: false
 contact_links:
+  - name: Get Help
+    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help
+    about: If you can't get something to work the way you expect, open a question in our discussion forums.
   - name: Feature Request
     url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas
     about: 'Suggest any ideas you have using our discussion forums.'
-  - name: Help
-    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help
-    about: 'If you have a question or need help, ask a question on the discussion forums.'
+  - name: Bug Report
+    url: https://github.com/tailwindlabs/tailwindcss/issues/new?body=%3C!--%20Please%20provide%20all%20of%20the%20information%20requested%20below.%20We%27re%20a%20small%20team%20and%20without%20all%20of%20this%20information%20it%27s%20not%20possible%20for%20us%20to%20help%20and%20your%20bug%20report%20will%20be%20closed.%20--%3E%0A%0A**What%20version%20of%20Tailwind%20CSS%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v2.0.4%0A%0A**What%20build%20tool%20(or%20framework%20if%20it%20abstracts%20the%20build%20tool)%20are%20you%20using%3F**%0A%0AFor%20example%3A%20postcss-cli%208.3.1%2C%20Next.js%2010.0.9%2C%20webpack%205.28.0%0A%0A**What%20version%20of%20Node.js%20are%20you%20using%3F**%0A%0AFor%20example%3A%20v12.0.0%0A%0A**What%20browser%20are%20you%20using%3F**%0A%0AFor%20example%3A%20Chrome%2C%20Safari%2C%20or%20N%2FA%0A%0A**What%20operating%20system%20are%20you%20using%3F**%0A%0AFor%20example%3A%20macOS%2C%20Windows%0A%0A**Reproduction%20URL**%0A%0AA%20Tailwind%20Play%20link%20or%20public%20GitHub%20repo%20that%20includes%20a%20minimal%20reproduction%20of%20the%20bug.%20**Please%20do%20not%20link%20to%20your%20actual%20project**%2C%20what%20we%20need%20instead%20is%20a%20_minimal_%20reproduction%20in%20a%20fresh%20project%20without%20any%20unnecessary%20code.%20This%20means%20it%20doesn%27t%20matter%20if%20your%20real%20project%20is%20private%2Fconfidential%2C%20since%20we%20want%20a%20link%20to%20a%20separate%2C%20isolated%20reproduction%20anyways.%20Unfortunately%20we%20can%27t%20provide%20support%20without%20a%20reproduction%2C%20and%20your%20issue%20will%20be%20closed%20with%20no%20comment%20if%20this%20is%20not%20provided.%0A%0A**Describe%20your%20issue**%0A%0ADescribe%20the%20problem%20you%27re%20seeing%2C%20any%20important%20steps%20to%20reproduce%20and%20what%20behavior%20you%20expect%20instead.
+    about: If you've already asked for help with a problem and confirmed something is broken with Tailwind CSS itself, create a bug report.
   - name: Documentation Issue
     url: https://github.com/tailwindlabs/tailwindcss.com
     about: 'For documentation issues, suggest changes on our documentation repository.'
-  - name: Kind Words
-    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=kind-words
-    about: "Have something nice to say about Tailwind? We'd love to hear it!"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45e11bea..27c43c0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,16 +13,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Throw when trying to `@apply` the `group` class ([#4666](https://github.com/tailwindlabs/tailwindcss/pull/4666))
 - Remove dependency on `modern-normalize`, inline and consolidate with Preflight ([#5358](https://github.com/tailwindlabs/tailwindcss/pull/5358))
 - Enable extended color palette by default with updated color names ([#5384](https://github.com/tailwindlabs/tailwindcss/pull/5384))
+- Move `vertical-align` values to config file instead of hard-coding ([#5487](https://github.com/tailwindlabs/tailwindcss/pull/5487))
 
 ### Added
 
 - 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))
+- Add `scroll-behavior` utilities ([#5388](https://github.com/tailwindlabs/tailwindcss/pull/5388))
+- Add `border-hidden` utility ([#5485](https://github.com/tailwindlabs/tailwindcss/pull/5485))
+- Add `align-sub` and `align-super` utilities by default ([#5486](https://github.com/tailwindlabs/tailwindcss/pull/5486))
+- Add `break-before`, `break-inside` and `break-after` utilities ([#5530](https://github.com/tailwindlabs/tailwindcss/pull/5530))
+
+### 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 +82,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 +1581,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/README.md b/README.md
index 3c7584ed..4d4ed895 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 <p>
     <a href="https://tailwindcss.com/" target="_blank">
-      <img alt="Tailwind CSS" width="350" src="https://refactoringui.nyc3.cdn.digitaloceanspaces.com/tailwind-logo.svg">
+      <img alt="Tailwind CSS" width="350" src="https://refactoringui.nyc3.cdn.digitaloceanspaces.com/tailwind-logo-sticker.svg">
     </a><br>
     A utility-first CSS framework for rapidly building custom user interfaces.
 </p>
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/defaultConfig.js b/defaultConfig.js
index 0a131abc..fb691d3a 100644
--- a/defaultConfig.js
+++ b/defaultConfig.js
@@ -1,4 +1,4 @@
-let { cloneDeep } = require('./src/util/cloneDeep')
+let { cloneDeep } = require('./lib/util/cloneDeep')
 let defaultConfig = require('./stubs/defaultConfig.stub.js')
 
 module.exports = cloneDeep(defaultConfig)
diff --git a/defaultTheme.js b/defaultTheme.js
index 4a42a998..ee7282c1 100644
--- a/defaultTheme.js
+++ b/defaultTheme.js
@@ -1,4 +1,4 @@
-let { cloneDeep } = require('./src/util/cloneDeep')
+let { cloneDeep } = require('./lib/util/cloneDeep')
 let defaultConfig = require('./stubs/defaultConfig.stub.js')
 
 module.exports = cloneDeep(defaultConfig.theme)
diff --git a/integrations/execute.js b/integrations/execute.js
index fccabcdd..bab80b4b 100644
--- a/integrations/execute.js
+++ b/integrations/execute.js
@@ -8,6 +8,15 @@ afterEach(() => {
   runningProcessess.splice(0).forEach((runningProcess) => runningProcess.stop())
 })
 
+function debounce(fn, ms) {
+  let state = { timer: undefined }
+
+  return (...args) => {
+    if (state.timer) clearTimeout(state.timer)
+    state.timer = setTimeout(() => fn(...args), ms)
+  }
+}
+
 module.exports = function $(command, options = {}) {
   let abortController = new AbortController()
   let cwd = resolveToolRoot()
@@ -37,13 +46,13 @@ module.exports = function $(command, options = {}) {
     }
   }
 
-  function notifyNextStdoutActor() {
+  let notifyNextStdoutActor = debounce(() => {
     return notifyNext(stdoutActors, stdoutMessages)
-  }
+  }, 200)
 
-  function notifyNextStderrActor() {
+  let notifyNextStderrActor = debounce(() => {
     return notifyNext(stderrActors, stderrMessages)
-  }
+  }, 200)
 
   let runningProcess = new Promise((resolve, reject) => {
     let child = spawn(command, args, {
@@ -74,6 +83,12 @@ module.exports = function $(command, options = {}) {
       combined += data
     })
 
+    child.on('error', (err) => {
+      if (err.name !== 'AbortError') {
+        throw err
+      }
+    })
+
     child.on('close', (code, signal) => {
       ;(signal === 'SIGTERM' ? resolve : code === 0 ? resolve : reject)({
         code,
diff --git a/integrations/io.js b/integrations/io.js
index f1a406ec..38377683 100644
--- a/integrations/io.js
+++ b/integrations/io.js
@@ -6,6 +6,17 @@ let chokidar = require('chokidar')
 
 let resolveToolRoot = require('./resolve-tool-root')
 
+function getWatcherOptions() {
+  return {
+    usePolling: true,
+    interval: 200,
+    awaitWriteFinish: {
+      stabilityThreshold: 1500,
+      pollInterval: 50,
+    },
+  }
+}
+
 module.exports = function ({
   /** Output directory, relative to the tool. */
   output = 'dist',
@@ -95,6 +106,10 @@ module.exports = function ({
       file = await resolveFile(file, absoluteOutputFolder)
       return fs.readFile(path.resolve(absoluteOutputFolder, file), 'utf8')
     },
+    async readInputFile(file) {
+      file = await resolveFile(file, absoluteInputFolder)
+      return fs.readFile(path.resolve(absoluteInputFolder, file), 'utf8')
+    },
     async appendToInputFile(file, contents) {
       let filePath = path.resolve(absoluteInputFolder, file)
       if (!fileCache[filePath]) {
@@ -122,48 +137,41 @@ module.exports = function ({
     async waitForOutputFileCreation(file) {
       if (file instanceof RegExp) {
         let r = file
-        let watcher = chokidar.watch(absoluteOutputFolder)
+        let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
 
         return new Promise((resolve) => {
           watcher.on('add', (file) => {
             if (r.test(file)) {
-              watcher.close()
-              resolve()
+              watcher.close().then(() => resolve())
             }
           })
         })
       } else {
         let filePath = path.resolve(absoluteOutputFolder, file)
-        let watcher = chokidar.watch(filePath)
 
-        let watcherPromise = new Promise((resolve) => {
-          watcher.once('add', () => {
-            watcher.close()
-            resolve()
+        return new Promise((resolve) => {
+          let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
+
+          watcher.on('add', (addedFile) => {
+            if (addedFile !== filePath) return
+            return watcher.close().finally(resolve)
           })
         })
-
-        if (existsSync(filePath)) {
-          watcher.close()
-          return Promise.resolve()
-        }
-
-        return watcherPromise
       }
     },
     async waitForOutputFileChange(file, cb = () => {}) {
       file = await resolveFile(file, absoluteOutputFolder)
-
       let filePath = path.resolve(absoluteOutputFolder, file)
-      let watcher = chokidar.watch(filePath)
 
       return new Promise((resolve) => {
-        let chain = Promise.resolve()
-        watcher.once('change', () => {
-          watcher.close()
-          chain.then(() => resolve())
-        })
-        chain.then(() => cb())
+        let watcher = chokidar.watch(absoluteOutputFolder, getWatcherOptions())
+
+        watcher
+          .on('change', (changedFile) => {
+            if (changedFile !== filePath) return
+            return watcher.close().finally(resolve)
+          })
+          .on('ready', cb)
       })
     },
   }
diff --git a/integrations/package-lock.json b/integrations/package-lock.json
index 2534f299..6824c194 100644
--- a/integrations/package-lock.json
+++ b/integrations/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "integrations",
       "version": "0.0.0",
       "devDependencies": {
         "isomorphic-fetch": "^3.0.0",
diff --git a/integrations/package.json b/integrations/package.json
index ba070a6d..20eed5b0 100644
--- a/integrations/package.json
+++ b/integrations/package.json
@@ -3,7 +3,7 @@
   "private": true,
   "version": "0.0.0",
   "scripts": {
-    "test": "jest --runInBand"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
     "testTimeout": 30000,
diff --git a/integrations/parcel/package-lock.json b/integrations/parcel/package-lock.json
index ef7f647e..e49a1892 100644
--- a/integrations/parcel/package-lock.json
+++ b/integrations/parcel/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "parcel",
       "version": "0.0.0",
       "devDependencies": {
         "parcel": "nightly"
diff --git a/integrations/parcel/package.json b/integrations/parcel/package.json
index 1bd16340..e4e88bd5 100644
--- a/integrations/parcel/package.json
+++ b/integrations/parcel/package.json
@@ -5,9 +5,10 @@
   "scripts": {
     "build": "parcel build ./src/index.html --no-cache",
     "dev": "parcel watch ./src/index.html --no-cache",
-    "test": "jest --runInBand"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "parcel",
     "setupFilesAfterEnv": [
       "<rootDir>/../../jest/customMatchers.js"
diff --git a/integrations/postcss-cli/package-lock.json b/integrations/postcss-cli/package-lock.json
index 038384ae..27fafd5c 100644
--- a/integrations/postcss-cli/package-lock.json
+++ b/integrations/postcss-cli/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "postcss-cli",
       "version": "0.0.0",
       "devDependencies": {
         "postcss": "^8.2.15",
diff --git a/integrations/postcss-cli/package.json b/integrations/postcss-cli/package.json
index bed103c0..6ad9adf0 100644
--- a/integrations/postcss-cli/package.json
+++ b/integrations/postcss-cli/package.json
@@ -3,10 +3,11 @@
   "private": true,
   "version": "0.0.0",
   "scripts": {
-    "build": "NODE_ENV=production postcss ./src/index.css -o ./dist/main.css",
-    "test": "jest --runInBand"
+    "build": "NODE_ENV=production postcss ./src/index.css -o ./dist/main.css --verbose",
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "PostCSS CLI",
     "setupFilesAfterEnv": [
       "<rootDir>/../../jest/customMatchers.js"
diff --git a/integrations/rollup/package.json b/integrations/rollup/package.json
index 9cfbcd1f..7234f08b 100644
--- a/integrations/rollup/package.json
+++ b/integrations/rollup/package.json
@@ -4,13 +4,12 @@
   "version": "0.0.0",
   "scripts": {
     "build": "rollup -c",
-    "test": "jest --runInBand"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "rollup.js",
-    "setupFilesAfterEnv": [
-      "<rootDir>/../../jest/customMatchers.js"
-    ]
+    "setupFilesAfterEnv": ["<rootDir>/../../jest/customMatchers.js"]
   },
   "devDependencies": {
     "rollup": "^2.48.0",
diff --git a/integrations/tailwindcss-cli/package-lock.json b/integrations/tailwindcss-cli/package-lock.json
index 7b7a3814..c121da5a 100644
--- a/integrations/tailwindcss-cli/package-lock.json
+++ b/integrations/tailwindcss-cli/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "tailwindcss-cli",
       "version": "0.0.0"
     }
   }
diff --git a/integrations/tailwindcss-cli/package.json b/integrations/tailwindcss-cli/package.json
index 882e30b4..0ed3dc1f 100644
--- a/integrations/tailwindcss-cli/package.json
+++ b/integrations/tailwindcss-cli/package.json
@@ -4,9 +4,10 @@
   "version": "0.0.0",
   "scripts": {
     "build": "NODE_ENV=production node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css",
-    "test": "jest --runInBand"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "Tailwind CSS CLI",
     "setupFilesAfterEnv": [
       "<rootDir>/../../jest/customMatchers.js"
diff --git a/integrations/vite/package-lock.json b/integrations/vite/package-lock.json
index dc99469e..3d1b0c24 100644
--- a/integrations/vite/package-lock.json
+++ b/integrations/vite/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "vite",
       "version": "0.0.0",
       "devDependencies": {
         "vite": "^2.3.3"
diff --git a/integrations/vite/package.json b/integrations/vite/package.json
index 9734a834..91fb4870 100644
--- a/integrations/vite/package.json
+++ b/integrations/vite/package.json
@@ -6,9 +6,10 @@
   "browser": "./src/index.js",
   "scripts": {
     "build": "vite build",
-    "test": "jest --runInBand"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "vite",
     "setupFilesAfterEnv": [
       "<rootDir>/../../jest/customMatchers.js"
diff --git a/integrations/webpack-4/package-lock.json b/integrations/webpack-4/package-lock.json
index 002e4190..ddb12373 100644
--- a/integrations/webpack-4/package-lock.json
+++ b/integrations/webpack-4/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "webpack-4",
       "version": "0.0.0",
       "devDependencies": {
         "css-loader": "^5.2.4",
diff --git a/integrations/webpack-4/package.json b/integrations/webpack-4/package.json
index 32d665c4..352229f4 100644
--- a/integrations/webpack-4/package.json
+++ b/integrations/webpack-4/package.json
@@ -7,9 +7,10 @@
   "scripts": {
     "build": "webpack --mode=production",
     "dev": "webpack --mode=development --watch",
-    "test": "jest --runInBand"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "webpack 4",
     "setupFilesAfterEnv": [
       "<rootDir>/../../jest/customMatchers.js"
diff --git a/integrations/webpack-5/package-lock.json b/integrations/webpack-5/package-lock.json
index 6960b0ee..1ef026ce 100644
--- a/integrations/webpack-5/package-lock.json
+++ b/integrations/webpack-5/package-lock.json
@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "webpack-5",
       "version": "0.0.0",
       "devDependencies": {
         "css-loader": "^5.2.4",
diff --git a/integrations/webpack-5/package.json b/integrations/webpack-5/package.json
index 061aab2b..8d88bbf1 100644
--- a/integrations/webpack-5/package.json
+++ b/integrations/webpack-5/package.json
@@ -7,9 +7,10 @@
   "scripts": {
     "build": "webpack --mode=production",
     "dev": "webpack --mode=development",
-    "test": "jest"
+    "test": "jest --runInBand --forceExit"
   },
   "jest": {
+    "testTimeout": 10000,
     "displayName": "webpack 5",
     "setupFilesAfterEnv": [
       "<rootDir>/../../jest/customMatchers.js"
diff --git a/package-lock.json b/package-lock.json
index bc23bb5d..8c13592e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,7 +41,7 @@
         "@babel/node": "^7.12.13",
         "@babel/preset-env": "^7.15.0",
         "@tailwindcss/aspect-ratio": "^0.2.1",
-        "@vercel/ncc": "^0.29.2",
+        "@vercel/ncc": "^0.31.1",
         "autoprefixer": "^10.3.3",
         "babel-jest": "^27.1.0",
         "cross-env": "^7.0.3",
@@ -53,7 +53,7 @@
         "jest-diff": "^27.1.0",
         "postcss": "^8.3.6",
         "postcss-cli": "^8.3.1",
-        "prettier": "^2.3.2",
+        "prettier": "^2.4.1",
         "rimraf": "^3.0.0"
       },
       "engines": {
@@ -2492,9 +2492,9 @@
       "dev": true
     },
     "node_modules/@vercel/ncc": {
-      "version": "0.29.2",
-      "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.29.2.tgz",
-      "integrity": "sha512-eUxibZD92k+rY0oZJFZooGqdVpGkDrrHlUhj9UAWrtoyXCGmNOWC1Kcr2KPrZoHRSlWwHOcRnkn2nGT+aHY2KA==",
+      "version": "0.31.1",
+      "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.31.1.tgz",
+      "integrity": "sha512-g0FAxwdViI6UzsiVz5HssIHqjcPa1EHL6h+2dcJD893SoCJaGdqqgUF09xnMW6goWnnhbLvgiKlgJWrJa+7qYA==",
       "dev": true,
       "bin": {
         "ncc": "dist/ncc/cli.js"
@@ -8533,9 +8533,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
-      "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
+      "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
       "dev": true,
       "bin": {
         "prettier": "bin-prettier.js"
@@ -12144,9 +12144,9 @@
       "dev": true
     },
     "@vercel/ncc": {
-      "version": "0.29.2",
-      "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.29.2.tgz",
-      "integrity": "sha512-eUxibZD92k+rY0oZJFZooGqdVpGkDrrHlUhj9UAWrtoyXCGmNOWC1Kcr2KPrZoHRSlWwHOcRnkn2nGT+aHY2KA==",
+      "version": "0.31.1",
+      "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.31.1.tgz",
+      "integrity": "sha512-g0FAxwdViI6UzsiVz5HssIHqjcPa1EHL6h+2dcJD893SoCJaGdqqgUF09xnMW6goWnnhbLvgiKlgJWrJa+7qYA==",
       "dev": true
     },
     "abab": {
@@ -16671,9 +16671,9 @@
       "dev": true
     },
     "prettier": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
-      "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
+      "integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
       "dev": true
     },
     "prettier-linter-helpers": {
diff --git a/package.json b/package.json
index 8ecad63d..aa8f20c9 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "@babel/node": "^7.12.13",
     "@babel/preset-env": "^7.15.0",
     "@tailwindcss/aspect-ratio": "^0.2.1",
-    "@vercel/ncc": "^0.29.2",
+    "@vercel/ncc": "^0.31.1",
     "autoprefixer": "^10.3.3",
     "babel-jest": "^27.1.0",
     "cross-env": "^7.0.3",
@@ -59,7 +59,7 @@
     "jest-diff": "^27.1.0",
     "postcss": "^8.3.6",
     "postcss-cli": "^8.3.1",
-    "prettier": "^2.3.2",
+    "prettier": "^2.4.1",
     "rimraf": "^3.0.0"
   },
   "peerDependencies": {
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..f89f923a 100644
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -72,6 +72,15 @@ export let pseudoElementVariants = ({ config, addVariant }) => {
     }),
   ])
 
+  addVariant(
+    'file',
+    transformAllSelectors((selector) => {
+      return updateAllClasses(selector, (className, { withPseudo }) => {
+        return withPseudo(`file${config('separator')}${className}`, '::file-selector-button')
+      })
+    })
+  )
+
   addVariant(
     'before',
     transformAllSelectors(
@@ -813,6 +822,41 @@ export let appearance = ({ addUtilities }) => {
 
 export let columns = createUtilityPlugin('columns', [['columns', ['columns']]])
 
+export let breakBefore = ({ addUtilities }) => {
+  addUtilities({
+    '.break-before-auto': { 'break-before': 'auto' },
+    '.break-before-avoid': { 'break-before': 'avoid' },
+    '.break-before-all': { 'break-before': 'all' },
+    '.break-before-avoid-page': { 'break-before': 'avoid-page' },
+    '.break-before-page': { 'break-before': 'page' },
+    '.break-before-left': { 'break-before': 'left' },
+    '.break-before-right': { 'break-before': 'right' },
+    '.break-before-column': { 'break-before': 'column' },
+  })
+}
+
+export let breakInside = ({ addUtilities }) => {
+  addUtilities({
+    '.break-inside-auto': { 'break-inside': 'auto' },
+    '.break-inside-avoid': { 'break-inside': 'avoid' },
+    '.break-inside-avoid-page': { 'break-inside': 'avoid-page' },
+    '.break-inside-avoid-column': { 'break-inside': 'avoid-column' },
+  })
+}
+
+export let breakAfter = ({ addUtilities }) => {
+  addUtilities({
+    '.break-after-auto': { 'break-after': 'auto' },
+    '.break-after-avoid': { 'break-after': 'avoid' },
+    '.break-after-all': { 'break-after': 'all' },
+    '.break-after-avoid-page': { 'break-after': 'avoid-page' },
+    '.break-after-page': { 'break-after': 'page' },
+    '.break-after-left': { 'break-after': 'left' },
+    '.break-after-right': { 'break-after': 'right' },
+    '.break-after-column': { 'break-after': 'column' },
+  })
+}
+
 export let gridAutoColumns = createUtilityPlugin('gridAutoColumns', [
   ['auto-cols', ['gridAutoColumns']],
 ])
@@ -1014,7 +1058,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 +1154,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' },
@@ -1168,15 +1223,17 @@ export let borderStyle = ({ addUtilities }) => {
     '.border-dashed': { 'border-style': 'dashed' },
     '.border-dotted': { 'border-style': 'dotted' },
     '.border-double': { 'border-style': 'double' },
+    '.border-hidden': { 'border-style': 'hidden' },
     '.border-none': { 'border-style': 'none' },
   })
 }
 
 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 +1250,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 +1272,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 +1285,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 +1298,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 +1311,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 +1339,9 @@ export let backgroundColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       bg: (value) => {
         if (!corePlugins('backgroundOpacity')) {
-          return { 'background-color': value }
+          return {
+            'background-color': toColorValue(value),
+          }
         }
 
         return withAlphaVariable({
@@ -1452,7 +1521,7 @@ export let textAlign = ({ addUtilities }) => {
 
 export let textIndent = createUtilityPlugin('textIndent', [['indent', ['text-indent']]])
 
-export let verticalAlign = ({ addUtilities }) => {
+export let verticalAlign = ({ addUtilities, matchUtilities }) => {
   addUtilities({
     '.align-baseline': { 'vertical-align': 'baseline' },
     '.align-top': { 'vertical-align': 'top' },
@@ -1460,7 +1529,18 @@ export let verticalAlign = ({ addUtilities }) => {
     '.align-bottom': { 'vertical-align': 'bottom' },
     '.align-text-top': { 'vertical-align': 'text-top' },
     '.align-text-bottom': { 'vertical-align': 'text-bottom' },
+    '.align-sub': { 'vertical-align': 'sub' },
+    '.align-super': { 'vertical-align': 'super' },
   })
+
+  matchUtilities(
+    {
+      align: (value) => ({
+        'vertical-align': value,
+      }),
+    },
+    { values: {}, type: 'any' }
+  )
 }
 
 export let fontFamily = createUtilityPlugin('fontFamily', [['font', ['fontFamily']]], {
@@ -1539,7 +1619,7 @@ export let textColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       text: (value) => {
         if (!corePlugins('textOpacity')) {
-          return { color: value }
+          return { color: toColorValue(value) }
         }
 
         return withAlphaVariable({
@@ -1583,7 +1663,11 @@ export let placeholderColor = ({ matchUtilities, theme, corePlugins }) => {
     {
       placeholder: (value) => {
         if (!corePlugins('placeholderOpacity')) {
-          return { '&::placeholder': { color: value } }
+          return {
+            '&::placeholder': {
+              color: toColorValue(value),
+            },
+          }
         }
 
         return {
@@ -2167,7 +2251,7 @@ export let transitionProperty = ({ matchUtilities, theme }) => {
         }
       },
     },
-    { values: theme('transitionProperty'), type: 'lookup' }
+    { values: theme('transitionProperty'), type: 'any' }
   )
 }
 
diff --git a/src/lib/detectNesting.js b/src/lib/detectNesting.js
new file mode 100644
index 00000000..5a3181a2
--- /dev/null
+++ b/src/lib/detectNesting.js
@@ -0,0 +1,20 @@
+export default function (_context) {
+  return (root, result) => {
+    let found = false
+
+    root.walkRules((rule) => {
+      if (found) return false
+
+      rule.walkRules((nestedRule) => {
+        found = true
+        nestedRule.warn(
+          result,
+          // TODO: Improve this warning message
+          'Nested CSS detected, checkout the docs on how to support nesting: https://tailwindcss.com/docs/using-with-preprocessors#nesting'
+        )
+
+        return false
+      })
+    })
+  }
+}
diff --git a/src/lib/resolveDefaultsAtRules.js b/src/lib/resolveDefaultsAtRules.js
index 95887b35..e8c1399b 100644
--- a/src/lib/resolveDefaultsAtRules.js
+++ b/src/lib/resolveDefaultsAtRules.js
@@ -3,7 +3,20 @@ import selectorParser from 'postcss-selector-parser'
 import { flagEnabled } from '../featureFlags'
 
 function minimumImpactSelector(nodes) {
-  let pseudos = nodes.filter((n) => n.type === 'pseudo')
+  let rest = nodes
+    // Keep all pseudo & combinator types (:not([hidden]) ~ :not([hidden]))
+    .filter((n) => n.type === 'pseudo' || n.type === 'combinator')
+    // Remove leading pseudo's (:hover, :focus, ...)
+    .filter((n, idx, all) => {
+      // Keep pseudo elements
+      if (n.type === 'pseudo' && n.value.startsWith('::')) return true
+
+      if (idx === 0 && n.type === 'pseudo') return false
+      if (idx > 0 && n.type === 'pseudo' && all[idx - 1].type === 'pseudo') return false
+
+      return true
+    })
+
   let [bestNode] = nodes
 
   for (let [type, getNode = (n) => n] of [
@@ -28,16 +41,12 @@ function minimumImpactSelector(nodes) {
     }
   }
 
-  return [bestNode, ...pseudos].join('').trim()
+  return [bestNode, ...rest].join('').trim()
 }
 
 let elementSelectorParser = selectorParser((selectors) => {
   return selectors.map((s) => {
-    let nodes = s
-      .split((n) => n.type === 'combinator')
-      .pop()
-      .filter((n) => n.type !== 'pseudo' || n.value.startsWith('::'))
-
+    let nodes = s.split((n) => n.type === 'combinator' && n.value === ' ').pop()
     return minimumImpactSelector(nodes)
   })
 })
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index 2762ce03..ed7c974d 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -9,13 +9,14 @@ import parseObjectStyles from '../util/parseObjectStyles'
 import prefixSelector from '../util/prefixSelector'
 import isPlainObject from '../util/isPlainObject'
 import escapeClassName from '../util/escapeClassName'
-import nameClass from '../util/nameClass'
+import nameClass, { formatClass } from '../util/nameClass'
 import { coerceValue } from '../util/pluginUtils'
 import bigSign from '../util/bigSign'
 import * as corePlugins from '../corePlugins'
 import * as sharedState from './sharedState'
 import { env } from './sharedState'
 import { toPath } from '../util/toPath'
+import log from '../util/log'
 
 function insertInto(list, value, { before = [] } = {}) {
   before = [].concat(before)
@@ -146,7 +147,7 @@ function isValidArbitraryValue(value) {
   return true
 }
 
-function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets }) {
+function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) {
   function getConfigValue(path, defaultValue) {
     return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig
   }
@@ -194,12 +195,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)) {
@@ -244,6 +242,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
         let prefixedIdentifier = prefixIdentifier(identifier, options)
         let offset = offsets.components++
 
+        classList.add(prefixedIdentifier)
+
         if (!context.candidateRuleMap.has(prefixedIdentifier)) {
           context.candidateRuleMap.set(prefixedIdentifier, [])
         }
@@ -271,6 +271,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
         let prefixedIdentifier = prefixIdentifier(identifier, options)
         let offset = offsets.utilities++
 
+        classList.add(prefixedIdentifier)
+
         if (!context.candidateRuleMap.has(prefixedIdentifier)) {
           context.candidateRuleMap.set(prefixedIdentifier, [])
         }
@@ -296,6 +298,8 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
         let prefixedIdentifier = prefixIdentifier(identifier, options)
         let rule = utilities[identifier]
 
+        classList.add([prefixedIdentifier, options])
+
         function wrapped(modifier) {
           let { type = 'any' } = options
           type = [].concat(type)
@@ -471,10 +475,13 @@ function registerPlugins(plugins, context) {
     user: 0n,
   }
 
+  let classList = new Set()
+
   let pluginApi = buildPluginApi(context.tailwindConfig, context, {
     variantList,
     variantMap,
     offsets,
+    classList,
   })
 
   for (let plugin of plugins) {
@@ -526,6 +533,83 @@ function registerPlugins(plugins, context) {
       variantFunctions.map((variantFunction, idx) => [sort << BigInt(idx), variantFunction])
     )
   }
+
+  //
+  let warnedAbout = new Set([])
+  context.safelist = function () {
+    let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean)
+    if (safelist.length <= 0) return []
+
+    let output = []
+    let checks = []
+
+    for (let value of safelist) {
+      if (typeof value === 'string') {
+        output.push(value)
+        continue
+      }
+
+      if (value instanceof RegExp) {
+        if (!warnedAbout.has('root-regex')) {
+          log.warn([
+            // TODO: Improve this warning message
+            'RegExp in the safelist option is not supported.',
+            'Please use the object syntax instead: https://tailwindcss.com/docs/...',
+          ])
+          warnedAbout.add('root-regex')
+        }
+        continue
+      }
+
+      checks.push(value)
+    }
+
+    if (checks.length <= 0) return output.map((value) => ({ raw: value, extension: 'html' }))
+
+    let patternMatchingCount = new Map()
+
+    for (let util of classList) {
+      let utils = Array.isArray(util)
+        ? (() => {
+            let [utilName, options] = util
+            return Object.keys(options?.values ?? {}).map((value) => formatClass(utilName, value))
+          })()
+        : [util]
+
+      for (let util of utils) {
+        for (let { pattern, variants = [] } of checks) {
+          // RegExp with the /g flag are stateful, so let's reset the last
+          // index pointer to reset the state.
+          pattern.lastIndex = 0
+
+          if (!patternMatchingCount.has(pattern)) {
+            patternMatchingCount.set(pattern, 0)
+          }
+
+          if (!pattern.test(util)) continue
+
+          patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1)
+
+          output.push(util)
+          for (let variant of variants) {
+            output.push(variant + context.tailwindConfig.separator + util)
+          }
+        }
+      }
+    }
+
+    for (let [regex, count] of patternMatchingCount.entries()) {
+      if (count !== 0) continue
+
+      log.warn([
+        // TODO: Improve this warning message
+        `You have a regex pattern in your "safelist" config (${regex}) that doesn't match any utilities.`,
+        'For more info, visit https://tailwindcss.com/docs/...',
+      ])
+    }
+
+    return output.map((value) => ({ raw: value, extension: 'html' }))
+  }
 }
 
 export function createContext(
diff --git a/src/lib/setupTrackingContext.js b/src/lib/setupTrackingContext.js
index f0ea6ab2..9b1e71ec 100644
--- a/src/lib/setupTrackingContext.js
+++ b/src/lib/setupTrackingContext.js
@@ -80,6 +80,7 @@ function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
   let changedContent = context.tailwindConfig.content.content
     .filter((item) => typeof item.raw === 'string')
     .concat(context.tailwindConfig.content.safelist)
+    .concat(context.safelist())
     .map(({ raw, extension }) => ({ content: raw, extension }))
 
   for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {
diff --git a/src/lib/setupWatchingContext.js b/src/lib/setupWatchingContext.js
index 4e9d772b..1eeeb11c 100644
--- a/src/lib/setupWatchingContext.js
+++ b/src/lib/setupWatchingContext.js
@@ -188,6 +188,7 @@ function resolvedChangedContent(context, candidateFiles) {
   let changedContent = context.tailwindConfig.content.content
     .filter((item) => typeof item.raw === 'string')
     .concat(context.tailwindConfig.content.safelist)
+    .concat(context.safelist())
     .map(({ raw, extension }) => ({ content: raw, extension }))
 
   for (let changedFile of resolveChangedFiles(context, candidateFiles)) {
diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js
index 2ac6374c..de28a894 100644
--- a/src/processTailwindFeatures.js
+++ b/src/processTailwindFeatures.js
@@ -5,6 +5,7 @@ import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
 import substituteScreenAtRules from './lib/substituteScreenAtRules'
 import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
 import collapseAdjacentRules from './lib/collapseAdjacentRules'
+import detectNesting from './lib/detectNesting'
 import { createContext } from './lib/setupContextUtils'
 
 export default function processTailwindFeatures(setupContext) {
@@ -31,6 +32,7 @@ export default function processTailwindFeatures(setupContext) {
       )
     }
 
+    detectNesting(context)(root, result)
     expandTailwindAtRules(context)(root, result)
     expandApplyAtRules(context)(root, result)
     evaluateTailwindFunctions(context)(root, result)
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/nameClass.js b/src/util/nameClass.js
index 2bb0548b..f67b3fd5 100644
--- a/src/util/nameClass.js
+++ b/src/util/nameClass.js
@@ -6,17 +6,21 @@ function asClass(name) {
 }
 
 export default function nameClass(classPrefix, key) {
+  return asClass(formatClass(classPrefix, key))
+}
+
+export function formatClass(classPrefix, key) {
   if (key === 'DEFAULT') {
-    return asClass(classPrefix)
+    return classPrefix
   }
 
   if (key === '-') {
-    return asClass(`-${classPrefix}`)
+    return `-${classPrefix}`
   }
 
   if (key.startsWith('-')) {
-    return asClass(`-${classPrefix}${key}`)
+    return `-${classPrefix}${key}`
   }
 
-  return asClass(`${classPrefix}-${key}`)
+  return `${classPrefix}-${key}`
 }
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/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
new file mode 100644
index 00000000..7e314924
--- /dev/null
+++ b/.github/workflows/integration-tests.yml
@@ -0,0 +1,59 @@
+name: Integration Tests
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    branches: [master]
+
+jobs:
+  test:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        integration: [parcel, postcss-cli, rollup, tailwindcss-cli, vite, webpack-4, webpack-5]
+        node-version: [16]
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ matrix.node-version }}
+
+      - name: Use cached node_modules (tailwindcss)
+        id: cache-tailwindcss
+        uses: actions/cache@v2
+        with:
+          path: node_modules
+          key: nodeModules-${{ hashFiles('**/package-lock.json') }}-${{ matrix.node-version }}-tailwindcss
+          restore-keys: |
+            nodeModules-
+
+      - name: Install dependencies
+        if: steps.cache-tailwindcss.outputs.cache-hit != 'true'
+        run: npm install
+        env:
+          CI: true
+
+      - name: Build Tailwind CSS
+        run: npm run prepublishOnly
+
+      - name: Use cached node_modules (integrations)
+        id: cache-integrations
+        uses: actions/cache@v2
+        with:
+          path: ./integrations/**/node_modules
+          key: nodeModules-${{ hashFiles('**/package-lock.json') }}-${{ matrix.node-version }}-${{ matrix.integration }}-integrations
+          restore-keys: |
+            nodeModules-
+
+      - name: Install shared dependencies
+        if: steps.cache-integrations.outputs.cache-hit != 'true'
+        run: npm run install:integrations
+        env:
+          CI: true
+
+      - name: Test ${{ matrix.integration }}
+        run: npm test --prefix ./integrations/${{ matrix.integration }}
diff --git a/integrations/parcel/tests/integration.test.js b/integrations/parcel/tests/integration.test.js
index f61536df..cece481b 100644
--- a/integrations/parcel/tests/integration.test.js
+++ b/integrations/parcel/tests/integration.test.js
@@ -80,7 +80,7 @@ describe.skip('watcher', () => {
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -240,7 +240,7 @@ describe.skip('watcher', () => {
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/postcss-cli/tests/integration.test.js b/integrations/postcss-cli/tests/integration.test.js
index 15681e3e..5f5cac96 100644
--- a/integrations/postcss-cli/tests/integration.test.js
+++ b/integrations/postcss-cli/tests/integration.test.js
@@ -1,13 +1,14 @@
 let $ = require('../../execute')
 let { css, html, javascript } = require('../../syntax')
 
-let {
-  readOutputFile,
-  appendToInputFile,
-  writeInputFile,
-  waitForOutputFileCreation,
-  waitForOutputFileChange,
-} = require('../../io')({ output: 'dist', input: 'src' })
+let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
+  output: 'dist',
+  input: 'src',
+})
+
+function ready(message) {
+  return message.includes('Finished')
+}
 
 describe('static build', () => {
   it('should be possible to generate tailwind output', async () => {
@@ -31,11 +32,10 @@ describe('watcher', () => {
   test('classes are generated when the html file changes', async () => {
     await writeInputFile('index.html', html`<div class="font-bold"></div>`)
 
-    let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w', {
+    let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w --verbose', {
       env: { TAILWIND_MODE: 'watch' },
     })
-
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -45,9 +45,8 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -60,15 +59,14 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -85,11 +83,11 @@ describe('watcher', () => {
   test('classes are generated when the tailwind.config.js file changes', async () => {
     await writeInputFile('index.html', html`<div class="font-bold md:font-medium"></div>`)
 
-    let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w', {
+    let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w --verbose', {
       env: { TAILWIND_MODE: 'watch' },
     })
 
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -104,10 +102,9 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await writeInputFile(
-        '../tailwind.config.js',
-        javascript`
+    await writeInputFile(
+      '../tailwind.config.js',
+      javascript`
           module.exports = {
             content: ['./src/index.html'],
             theme: {
@@ -126,8 +123,8 @@ describe('watcher', () => {
             plugins: [],
           }
       `
-      )
-    })
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -148,11 +145,11 @@ describe('watcher', () => {
   test('classes are generated when the index.css file changes', async () => {
     await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
 
-    let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w', {
+    let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w --verbose', {
       env: { TAILWIND_MODE: 'watch' },
     })
 
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -162,22 +159,21 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await writeInputFile(
-        'index.css',
-        css`
-          @tailwind base;
-          @tailwind components;
-          @tailwind utilities;
-
-          @layer components {
-            .btn {
-              @apply px-2 py-1 rounded;
-            }
+    await writeInputFile(
+      'index.css',
+      css`
+        @tailwind base;
+        @tailwind components;
+        @tailwind utilities;
+
+        @layer components {
+          .btn {
+            @apply px-2 py-1 rounded;
           }
-        `
-      )
-    })
+        }
+      `
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -194,29 +190,28 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await writeInputFile(
-        'index.css',
-        css`
-          @tailwind base;
-          @tailwind components;
-          @tailwind utilities;
-
-          @layer components {
-            .btn {
-              @apply px-2 py-1 rounded bg-red-500;
-            }
+    await writeInputFile(
+      'index.css',
+      css`
+        @tailwind base;
+        @tailwind components;
+        @tailwind utilities;
+
+        @layer components {
+          .btn {
+            @apply px-2 py-1 rounded bg-red-500;
           }
-        `
-      )
-    })
+        }
+      `
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/rollup/tests/integration.test.js b/integrations/rollup/tests/integration.test.js
index ab17a5d5..0e52a2ec 100644
--- a/integrations/rollup/tests/integration.test.js
+++ b/integrations/rollup/tests/integration.test.js
@@ -1,13 +1,14 @@
 let $ = require('../../execute')
 let { css, html, javascript } = require('../../syntax')
 
-let {
-  readOutputFile,
-  appendToInputFile,
-  writeInputFile,
-  waitForOutputFileCreation,
-  waitForOutputFileChange,
-} = require('../../io')({ output: 'dist', input: 'src' })
+let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
+  output: 'dist',
+  input: 'src',
+})
+
+function ready(message) {
+  return message.includes('created')
+}
 
 describe('static build', () => {
   it('should be possible to generate tailwind output', async () => {
@@ -32,8 +33,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
     await writeInputFile('index.html', html`<div class="font-bold"></div>`)
 
     let runningProcess = $('rollup -c --watch', { env })
-
-    await waitForOutputFileCreation('index.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
@@ -43,9 +43,8 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       `
     )
 
-    await waitForOutputFileChange('index.css', async () => {
-      await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
@@ -58,15 +57,14 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       `
     )
 
-    await waitForOutputFileChange('index.css', async () => {
-      await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -84,8 +82,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
     await writeInputFile('index.html', html`<div class="font-bold md:font-medium"></div>`)
 
     let runningProcess = $('rollup -c --watch', { env })
-
-    await waitForOutputFileCreation('index.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
@@ -100,10 +97,9 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       `
     )
 
-    await waitForOutputFileChange('index.css', async () => {
-      await writeInputFile(
-        '../tailwind.config.js',
-        javascript`
+    await writeInputFile(
+      '../tailwind.config.js',
+      javascript`
           module.exports = {
             content: ['./src/index.html'],
             theme: {
@@ -122,8 +118,8 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
             plugins: [],
           }
       `
-      )
-    })
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
@@ -145,8 +141,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
     await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
 
     let runningProcess = $('rollup -c --watch', { env })
-
-    await waitForOutputFileCreation('index.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
@@ -156,22 +151,21 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       `
     )
 
-    await waitForOutputFileChange('index.css', async () => {
-      await writeInputFile(
-        'index.css',
-        css`
-          @tailwind base;
-          @tailwind components;
-          @tailwind utilities;
-
-          @layer components {
-            .btn {
-              @apply px-2 py-1 rounded;
-            }
+    await writeInputFile(
+      'index.css',
+      css`
+        @tailwind base;
+        @tailwind components;
+        @tailwind utilities;
+
+        @layer components {
+          .btn {
+            @apply px-2 py-1 rounded;
           }
-        `
-      )
-    })
+        }
+      `
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
@@ -188,29 +182,28 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       `
     )
 
-    await waitForOutputFileChange('index.css', async () => {
-      await writeInputFile(
-        'index.css',
-        css`
-          @tailwind base;
-          @tailwind components;
-          @tailwind utilities;
-
-          @layer components {
-            .btn {
-              @apply px-2 py-1 rounded bg-red-500;
-            }
+    await writeInputFile(
+      'index.css',
+      css`
+        @tailwind base;
+        @tailwind components;
+        @tailwind utilities;
+
+        @layer components {
+          .btn {
+            @apply px-2 py-1 rounded bg-red-500;
           }
-        `
-      )
-    })
+        }
+      `
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('index.css')).toIncludeCss(
       css`
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/tailwindcss-cli/tests/cli.test.js b/integrations/tailwindcss-cli/tests/cli.test.js
index b73fc61e..fb39c944 100644
--- a/integrations/tailwindcss-cli/tests/cli.test.js
+++ b/integrations/tailwindcss-cli/tests/cli.test.js
@@ -187,7 +187,7 @@ describe('Build command', () => {
 
         .btn-after {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
@@ -238,7 +238,7 @@ describe('Build command', () => {
 
         .btn-after {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/tailwindcss-cli/tests/integration.test.js b/integrations/tailwindcss-cli/tests/integration.test.js
index c0808057..72c80378 100644
--- a/integrations/tailwindcss-cli/tests/integration.test.js
+++ b/integrations/tailwindcss-cli/tests/integration.test.js
@@ -1,13 +1,14 @@
 let $ = require('../../execute')
 let { css, html, javascript } = require('../../syntax')
 
-let {
-  readOutputFile,
-  appendToInputFile,
-  writeInputFile,
-  waitForOutputFileCreation,
-  waitForOutputFileChange,
-} = require('../../io')({ output: 'dist', input: 'src' })
+let { readOutputFile, appendToInputFile, writeInputFile } = require('../../io')({
+  output: 'dist',
+  input: 'src',
+})
+
+function ready(message) {
+  return message.includes('Done in')
+}
 
 describe('static build', () => {
   it('should be possible to generate tailwind output', async () => {
@@ -48,22 +49,20 @@ describe('static build', () => {
       `
     )
 
-    $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css', {
+    await $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css', {
       env: { NODE_ENV: 'production' },
     })
 
-    await waitForOutputFileCreation('main.css')
-
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
 
         .bg-red-600 {
           --tw-bg-opacity: 1;
-          background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
+          background-color: rgb(220 38 38 / var(--tw-bg-opacity));
         }
 
         .font-bold {
@@ -79,8 +78,7 @@ describe('watcher', () => {
     await writeInputFile('index.html', html`<div class="font-bold"></div>`)
 
     let runningProcess = $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css -w')
-
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -90,9 +88,8 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -105,15 +102,14 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="bg-red-500"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -145,8 +141,7 @@ describe('watcher', () => {
     )
 
     let runningProcess = $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css -w')
-
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -156,9 +151,8 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
-    })
+    await appendToInputFile('index.html', html`<div class="font-normal"></div>`)
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).not.toIncludeCss(css`
       @layer base {
@@ -186,8 +180,7 @@ describe('watcher', () => {
     await writeInputFile('index.html', html`<div class="font-bold md:font-medium"></div>`)
 
     let runningProcess = $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css -w')
-
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -202,10 +195,9 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await writeInputFile(
-        '../tailwind.config.js',
-        javascript`
+    await writeInputFile(
+      '../tailwind.config.js',
+      javascript`
           module.exports = {
             content: ['./src/index.html'],
             theme: {
@@ -224,8 +216,8 @@ describe('watcher', () => {
             plugins: [],
           }
       `
-      )
-    })
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -247,8 +239,7 @@ describe('watcher', () => {
     await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
 
     let runningProcess = $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css -w')
-
-    await waitForOutputFileCreation('main.css')
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -258,22 +249,21 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await writeInputFile(
-        'index.css',
-        css`
-          @tailwind base;
-          @tailwind components;
-          @tailwind utilities;
-
-          @layer components {
-            .btn {
-              @apply px-2 py-1 rounded;
-            }
+    await writeInputFile(
+      'index.css',
+      css`
+        @tailwind base;
+        @tailwind components;
+        @tailwind utilities;
+
+        @layer components {
+          .btn {
+            @apply px-2 py-1 rounded;
           }
-        `
-      )
-    })
+        }
+      `
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
@@ -290,29 +280,28 @@ describe('watcher', () => {
       `
     )
 
-    await waitForOutputFileChange('main.css', async () => {
-      await writeInputFile(
-        'index.css',
-        css`
-          @tailwind base;
-          @tailwind components;
-          @tailwind utilities;
-
-          @layer components {
-            .btn {
-              @apply px-2 py-1 rounded bg-red-500;
-            }
+    await writeInputFile(
+      'index.css',
+      css`
+        @tailwind base;
+        @tailwind components;
+        @tailwind utilities;
+
+        @layer components {
+          .btn {
+            @apply px-2 py-1 rounded bg-red-500;
           }
-        `
-      )
-    })
+        }
+      `
+    )
+    await runningProcess.onStderr(ready)
 
     expect(await readOutputFile('main.css')).toIncludeCss(
       css`
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/vite/tests/integration.test.js b/integrations/vite/tests/integration.test.js
index 18ff59c7..17a5ebb6 100644
--- a/integrations/vite/tests/integration.test.js
+++ b/integrations/vite/tests/integration.test.js
@@ -87,7 +87,7 @@ describe('watcher', () => {
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -242,7 +242,7 @@ describe('watcher', () => {
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/webpack-4/tests/integration.test.js b/integrations/webpack-4/tests/integration.test.js
index ce1d5dba..929a8bd7 100644
--- a/integrations/webpack-4/tests/integration.test.js
+++ b/integrations/webpack-4/tests/integration.test.js
@@ -64,7 +64,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -208,7 +208,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
diff --git a/integrations/webpack-5/tests/integration.test.js b/integrations/webpack-5/tests/integration.test.js
index 6112b7bf..b6f65452 100644
--- a/integrations/webpack-5/tests/integration.test.js
+++ b/integrations/webpack-5/tests/integration.test.js
@@ -64,7 +64,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
         .font-bold {
           font-weight: 700;
@@ -208,7 +208,7 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
         .btn {
           border-radius: 0.25rem;
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
           padding-left: 0.5rem;
           padding-right: 0.5rem;
           padding-top: 0.25rem;
@@ -253,12 +253,12 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
       css`
         .bg-red-500 {
           --tw-bg-opacity: 1;
-          background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
+          background-color: rgb(239 68 68 / var(--tw-bg-opacity));
         }
 
         .bg-red-600 {
           --tw-bg-opacity: 1;
-          background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
+          background-color: rgb(220 38 38 / var(--tw-bg-opacity));
         }
 
         .font-bold {
diff --git a/tests/arbitrary-values.test.css b/tests/arbitrary-values.test.css
index 3eb51fa9..9188a594 100644
--- a/tests/arbitrary-values.test.css
+++ b/tests/arbitrary-values.test.css
@@ -323,6 +323,9 @@
 .indent-\[var\(--indent\)\] {
   text-indent: var(--indent);
 }
+.align-\[10em\] {
+  vertical-align: 10em;
+}
 .text-\[2\.23rem\] {
   font-size: 2.23rem;
 }
@@ -448,6 +451,11 @@
   --tw-backdrop-sepia: sepia(0.38);
   backdrop-filter: var(--tw-backdrop-filter);
 }
+.transition-\[opacity\2c width\] {
+  transition-property: opacity, width;
+  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+  transition-duration: 150ms;
+}
 .delay-\[var\(--delay\)\] {
   transition-delay: var(--delay);
 }
diff --git a/tests/arbitrary-values.test.html b/tests/arbitrary-values.test.html
index 3c55dd90..65f93df5 100644
--- a/tests/arbitrary-values.test.html
+++ b/tests/arbitrary-values.test.html
@@ -56,10 +56,12 @@
     <div class="skew-x-[3px]"></div>
     <div class="skew-y-[3px]"></div>
     <div class="indent-[50%] indent-[var(--indent)]"></div>
+    <div class="align-[10em]"></div>
     <div class="text-[2.23rem]"></div>
     <div class="text-[length:var(--font-size)]"></div>
     <div class="text-[color:var(--color)]"></div>
     <div class="text-[angle:var(--angle)]"></div>
+    <div class="transition-[opacity,width]"></div>
     <div class="duration-[2s]"></div>
     <div class="m-[7px]"></div>
     <div class="mx-[7px]"></div>
@@ -71,10 +73,20 @@
     <div class="mt-[clamp(30px,100px)]"></div>
     <div class="rounded-[11px]"></div>
     <div
-      class="rounded-t-[var(--radius)] rounded-r-[var(--radius)] rounded-b-[var(--radius)] rounded-l-[var(--radius)]"
+      class="
+        rounded-t-[var(--radius)]
+        rounded-r-[var(--radius)]
+        rounded-b-[var(--radius)]
+        rounded-l-[var(--radius)]
+      "
     ></div>
     <div
-      class="rounded-tr-[var(--radius)] rounded-br-[var(--radius)] rounded-bl-[var(--radius)] rounded-tl-[var(--radius)]"
+      class="
+        rounded-tr-[var(--radius)]
+        rounded-br-[var(--radius)]
+        rounded-bl-[var(--radius)]
+        rounded-tl-[var(--radius)]
+      "
     ></div>
     <div class="duration-[var(--app-duration)]"></div>
     <div class="p-[var(--app-padding)]"></div>
@@ -124,28 +136,51 @@
     <div class="accent-[var(--accent-color)]"></div>
 
     <!-- Balancing issues, this is not checking the validity of the actual value, but purely syntax-wise -->
-    <div class="w-[do-not-generate-this]w-[it-is-invalid-syntax]"></div><!-- INVALID -->
-    <div class="grid-cols-[[linename],1fr,auto]"></div><!-- VALID -->
-    <div class="w-[{}]"></div><!-- VALID -->
-    <div class="w-[{{}}]"></div><!-- VALID -->
-    <div class="w-[[]]"></div><!-- VALID -->
-    <div class="w-[[[]]]"></div><!-- VALID -->
-    <div class="w-[()]"></div><!-- VALID -->
-    <div class="w-[(())]"></div><!-- VALID -->
-    <div class="w-[][]"></div><!-- INVALID -->
-    <div class="w-[)(]"></div><!-- INVALID -->
-    <div class="w-[}{]"></div><!-- INVALID -->
-    <div class="w-[][]]"></div><!-- INVALID -->
-    <div class="w-[)()]"></div><!-- INVALID -->
-    <div class="w-[}{}]"></div><!-- INVALID -->
-    <div class="w-['][]']"></div><!-- VALID -->
-    <div class="w-[')()']"></div><!-- VALID -->
-    <div class="w-['}{}']"></div><!-- VALID -->
-    <div class="w-[{[}]]"></div><!-- INVALID -->
-    <div class="w-[[{]}]"></div><!-- INVALID -->
-    <div class="w-[{(})]"></div><!-- INVALID -->
-    <div class="w-[({)}]"></div><!-- INVALID -->
-    <div class="w-[([)]]"></div><!-- INVALID -->
-    <div class="w-[[(])]"></div><!-- INVALID -->
+    <div class="w-[do-not-generate-this]w-[it-is-invalid-syntax]"></div>
+    <!-- INVALID -->
+    <div class="grid-cols-[[linename],1fr,auto]"></div>
+    <!-- VALID -->
+    <div class="w-[{}]"></div>
+    <!-- VALID -->
+    <div class="w-[{{}}]"></div>
+    <!-- VALID -->
+    <div class="w-[[]]"></div>
+    <!-- VALID -->
+    <div class="w-[[[]]]"></div>
+    <!-- VALID -->
+    <div class="w-[()]"></div>
+    <!-- VALID -->
+    <div class="w-[(())]"></div>
+    <!-- VALID -->
+    <div class="w-[][]"></div>
+    <!-- INVALID -->
+    <div class="w-[)(]"></div>
+    <!-- INVALID -->
+    <div class="w-[}{]"></div>
+    <!-- INVALID -->
+    <div class="w-[][]]"></div>
+    <!-- INVALID -->
+    <div class="w-[)()]"></div>
+    <!-- INVALID -->
+    <div class="w-[}{}]"></div>
+    <!-- INVALID -->
+    <div class="w-['][]']"></div>
+    <!-- VALID -->
+    <div class="w-[')()']"></div>
+    <!-- VALID -->
+    <div class="w-['}{}']"></div>
+    <!-- VALID -->
+    <div class="w-[{[}]]"></div>
+    <!-- INVALID -->
+    <div class="w-[[{]}]"></div>
+    <!-- INVALID -->
+    <div class="w-[{(})]"></div>
+    <!-- INVALID -->
+    <div class="w-[({)}]"></div>
+    <!-- INVALID -->
+    <div class="w-[([)]]"></div>
+    <!-- INVALID -->
+    <div class="w-[[(])]"></div>
+    <!-- INVALID -->
   </body>
 </html>
diff --git a/tests/basic-usage.test.css b/tests/basic-usage.test.css
index 06f7b030..00bb9e41 100644
--- a/tests/basic-usage.test.css
+++ b/tests/basic-usage.test.css
@@ -320,6 +320,15 @@
 .columns-md {
   columns: 28rem;
 }
+.break-before-page  {
+  break-before: page;
+}
+.break-inside-avoid-column  {
+  break-inside: avoid-column;
+}
+.break-after-auto {
+  break-after: auto;
+}
 .auto-cols-min {
   grid-auto-columns: min-content;
 }
@@ -461,6 +470,9 @@
 .border-solid {
   border-style: solid;
 }
+.border-hidden {
+  border-style: hidden;
+}
 .border-black {
   --tw-border-opacity: 1;
   border-color: rgb(0 0 0 / var(--tw-border-opacity));
diff --git a/tests/basic-usage.test.html b/tests/basic-usage.test.html
index 69ce7b89..01103fc3 100644
--- a/tests/basic-usage.test.html
+++ b/tests/basic-usage.test.html
@@ -28,7 +28,7 @@
     <div class="border-black border-t-black border-r-black border-b-black border-l-black"></div>
     <div class="border-opacity-10"></div>
     <div class="rounded-md"></div>
-    <div class="border-solid"></div>
+    <div class="border-solid border-hidden"></div>
     <div class="border"></div>
     <div class="border-2 border-t border-b-4"></div>
     <div class="shadow"></div>
@@ -62,6 +62,7 @@
     <div class="gap-x-2 gap-y-3 gap-4"></div>
     <div class="from-red-300 via-purple-200 to-blue-400"></div>
     <div class="columns-1 columns-md"></div>
+    <div class="break-before-page break-inside-avoid-column break-after-auto"></div>
     <div class="auto-cols-min"></div>
     <div class="grid-flow-row"></div>
     <div class="auto-rows-max"></div>
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/detect-nesting.test.js b/tests/detect-nesting.test.js
new file mode 100644
index 00000000..35e30393
--- /dev/null
+++ b/tests/detect-nesting.test.js
@@ -0,0 +1,73 @@
+import { run, html, css } from './util/run'
+
+it('should warn when we detect nested css', () => {
+  let config = {
+    content: [{ raw: html`<div class="nested"></div>` }],
+  }
+
+  let input = css`
+    @tailwind utilities;
+
+    .nested {
+      .example {
+      }
+    }
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.messages).toHaveLength(1)
+    expect(result.messages).toMatchObject([
+      {
+        type: 'warning',
+        text: 'Nested CSS detected, checkout the docs on how to support nesting: https://tailwindcss.com/docs/using-with-preprocessors#nesting',
+      },
+    ])
+  })
+})
+
+it('should not warn when nesting a single rule inside a media query', () => {
+  let config = {
+    content: [{ raw: html`<div class="nested"></div>` }],
+  }
+
+  let input = css`
+    @tailwind utilities;
+
+    @media (min-width: 768px) {
+      .nested {
+      }
+    }
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.messages).toHaveLength(0)
+    expect(result.messages).toEqual([])
+  })
+})
+
+it('should only warn for the first detected nesting ', () => {
+  let config = {
+    content: [{ raw: html`<div class="nested other"></div>` }],
+  }
+
+  let input = css`
+    @tailwind utilities;
+
+    .nested {
+      .example {
+      }
+
+      .other {
+      }
+    }
+
+    .other {
+      .example {
+      }
+    }
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.messages).toHaveLength(1)
+  })
+})
diff --git a/tests/experimental.test.js b/tests/experimental.test.js
new file mode 100644
index 00000000..6404c001
--- /dev/null
+++ b/tests/experimental.test.js
@@ -0,0 +1,209 @@
+import { run, html, css } from './util/run'
+
+test('experimental universal selector improvements (box-shadow)', () => {
+  let config = {
+    experimental: 'all',
+    content: [{ raw: html`<div class="shadow resize"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind base;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .shadow {
+        --tw-ring-offset-shadow: 0 0 #0000;
+        --tw-ring-shadow: 0 0 #0000;
+        --tw-shadow: 0 0 #0000;
+      }
+
+      .resize {
+        resize: both;
+      }
+
+      .shadow {
+        --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
+        box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+          var(--tw-shadow);
+      }
+    `)
+  })
+})
+
+test('experimental universal selector improvements (pseudo hover)', () => {
+  let config = {
+    experimental: 'all',
+    content: [{ raw: html`<div class="hover:shadow resize"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind base;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .hover\\:shadow {
+        --tw-ring-offset-shadow: 0 0 #0000;
+        --tw-ring-shadow: 0 0 #0000;
+        --tw-shadow: 0 0 #0000;
+      }
+
+      .resize {
+        resize: both;
+      }
+
+      .hover\\:shadow:hover {
+        --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
+        box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+          var(--tw-shadow);
+      }
+    `)
+  })
+})
+
+test('experimental universal selector improvements (multiple classes: group)', () => {
+  let config = {
+    experimental: 'all',
+    content: [{ raw: html`<div class="group-hover:shadow resize"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind base;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .group-hover\\:shadow {
+        --tw-ring-offset-shadow: 0 0 #0000;
+        --tw-ring-shadow: 0 0 #0000;
+        --tw-shadow: 0 0 #0000;
+      }
+
+      .resize {
+        resize: both;
+      }
+
+      .group:hover .group-hover\\:shadow {
+        --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
+        box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+          var(--tw-shadow);
+      }
+    `)
+  })
+})
+
+test('experimental universal selector improvements (child selectors: divide-y)', () => {
+  let config = {
+    experimental: 'all',
+    content: [{ raw: html`<div class="divide-y resize"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind base;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .divide-y > :not([hidden]) ~ :not([hidden]) {
+        --tw-border-opacity: 1;
+        border-color: rgb(229 231 235 / var(--tw-border-opacity));
+      }
+
+      .resize {
+        resize: both;
+      }
+
+      .divide-y > :not([hidden]) ~ :not([hidden]) {
+        --tw-divide-y-reverse: 0;
+        border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
+        border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
+      }
+    `)
+  })
+})
+
+test('experimental universal selector improvements (hover:divide-y)', () => {
+  let config = {
+    experimental: 'all',
+    content: [{ raw: html`<div class="hover:divide-y resize"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind base;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .hover\\:divide-y > :not([hidden]) ~ :not([hidden]) {
+        --tw-border-opacity: 1;
+        border-color: rgb(229 231 235 / var(--tw-border-opacity));
+      }
+
+      .resize {
+        resize: both;
+      }
+
+      .hover\\:divide-y:hover > :not([hidden]) ~ :not([hidden]) {
+        --tw-divide-y-reverse: 0;
+        border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
+        border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
+      }
+    `)
+  })
+})
+
+test('experimental universal selector improvements (#app important)', () => {
+  let config = {
+    experimental: 'all',
+    important: '#app',
+    content: [{ raw: html`<div class="shadow divide-y resize"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind base;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .divide-y > :not([hidden]) ~ :not([hidden]) {
+        --tw-border-opacity: 1;
+        border-color: rgb(229 231 235 / var(--tw-border-opacity));
+      }
+
+      .shadow {
+        --tw-ring-offset-shadow: 0 0 #0000;
+        --tw-ring-shadow: 0 0 #0000;
+        --tw-shadow: 0 0 #0000;
+      }
+
+      #app .resize {
+        resize: both;
+      }
+
+      #app .divide-y > :not([hidden]) ~ :not([hidden]) {
+        --tw-divide-y-reverse: 0;
+        border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
+        border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
+      }
+
+      #app .shadow {
+        --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px 0 rgb(0 0 0 / 0.06);
+        box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
+          var(--tw-shadow);
+      }
+    `)
+  })
+})
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/resolve-defaults-at-rules.test.js b/tests/resolve-defaults-at-rules.test.js
index 0a4d2af1..6f250bc6 100644
--- a/tests/resolve-defaults-at-rules.test.js
+++ b/tests/resolve-defaults-at-rules.test.js
@@ -527,6 +527,7 @@ test('when a utility uses defaults but they do not exist', async () => {
 
 test('selectors are reduced to the lowest possible specificity', async () => {
   let config = {
+    experimental: 'all',
     content: [{ raw: html`<div class="foo"></div>` }],
     corePlugins: [],
   }
@@ -576,9 +577,13 @@ test('selectors are reduced to the lowest possible specificity', async () => {
 
   return run(input, config).then((result) => {
     expect(result.css).toMatchFormattedCss(css`
-      *,
-      ::before,
-      ::after {
+      .foo,
+      [id='app'],
+      [id='page'],
+      [id='other'],
+      [data-bar='baz'],
+      article,
+      [id='another']::before {
         --color: black;
       }
 
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/safelist.test.js b/tests/safelist.test.js
new file mode 100644
index 00000000..6325cfad
--- /dev/null
+++ b/tests/safelist.test.js
@@ -0,0 +1,196 @@
+import { run, html, css } from './util/run'
+
+it('should not safelist anything', () => {
+  let config = {
+    content: [{ raw: html`<div class="uppercase"></div>` }],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .uppercase {
+        text-transform: uppercase;
+      }
+    `)
+  })
+})
+
+it('should safelist strings', () => {
+  let config = {
+    content: [{ raw: html`<div class="uppercase"></div>` }],
+    safelist: ['mt-[20px]', 'font-bold', 'text-gray-200', 'hover:underline'],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .mt-\\[20px\\] {
+        margin-top: 20px;
+      }
+
+      .font-bold {
+        font-weight: 700;
+      }
+
+      .uppercase {
+        text-transform: uppercase;
+      }
+
+      .text-gray-200 {
+        --tw-text-opacity: 1;
+        color: rgb(229 231 235 / var(--tw-text-opacity));
+      }
+
+      .hover\\:underline:hover {
+        text-decoration: underline;
+      }
+    `)
+  })
+})
+
+it('should safelist based on a pattern regex', () => {
+  let config = {
+    content: [{ raw: html`<div class="uppercase"></div>` }],
+    safelist: [
+      {
+        pattern: /bg-(red)-(100|200)/,
+        variants: ['hover'],
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .bg-red-100 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 226 226 / var(--tw-bg-opacity));
+      }
+
+      .bg-red-200 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 202 202 / var(--tw-bg-opacity));
+      }
+
+      .uppercase {
+        text-transform: uppercase;
+      }
+
+      .hover\\:bg-red-100:hover {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 226 226 / var(--tw-bg-opacity));
+      }
+
+      .hover\\:bg-red-200:hover {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 202 202 / var(--tw-bg-opacity));
+      }
+    `)
+  })
+})
+
+it('should not generate duplicates', () => {
+  let config = {
+    content: [{ raw: html`<div class="uppercase"></div>` }],
+    safelist: [
+      'uppercase',
+      {
+        pattern: /bg-(red)-(100|200)/,
+        variants: ['hover'],
+      },
+      {
+        pattern: /bg-(red)-(100|200)/,
+        variants: ['hover'],
+      },
+      {
+        pattern: /bg-(red)-(100|200)/,
+        variants: ['hover'],
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .bg-red-100 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 226 226 / var(--tw-bg-opacity));
+      }
+
+      .bg-red-200 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 202 202 / var(--tw-bg-opacity));
+      }
+
+      .uppercase {
+        text-transform: uppercase;
+      }
+
+      .hover\\:bg-red-100:hover {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 226 226 / var(--tw-bg-opacity));
+      }
+
+      .hover\\:bg-red-200:hover {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 202 202 / var(--tw-bg-opacity));
+      }
+    `)
+  })
+})
+
+it('should safelist when using a custom prefix', () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`<div class="tw-uppercase"></div>` }],
+    safelist: [
+      {
+        pattern: /tw-bg-red-(100|200)/g,
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .tw-bg-red-100 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 226 226 / var(--tw-bg-opacity));
+      }
+
+      .tw-bg-red-200 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(254 202 202 / var(--tw-bg-opacity));
+      }
+
+      .tw-uppercase {
+        text-transform: uppercase;
+      }
+    `)
+  })
+})
+
+it('should not safelist when an empty list is provided', () => {
+  let config = {
+    content: [{ raw: html`<div class="uppercase"></div>` }],
+    safelist: [],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .uppercase {
+        text-transform: uppercase;
+      }
+    `)
+  })
+})
+
+it('should not safelist when an sparse/holey list is provided', () => {
+  let config = {
+    content: [{ raw: html`<div class="uppercase"></div>` }],
+    safelist: [, , ,],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchCss(css`
+      .uppercase {
+        text-transform: uppercase;
+      }
+    `)
+  })
+})
diff --git a/tests/svelte-syntax.test.css b/tests/svelte-syntax.test.css
deleted file mode 100644
index 20aaf36f..00000000
--- a/tests/svelte-syntax.test.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.bg-red-500 {
-  --tw-bg-opacity: 1;
-  background-color: rgb(239 68 68 / var(--tw-bg-opacity));
-}
-@media (min-width: 1024px) {
-  .lg\:hover\:bg-blue-500:hover {
-    --tw-bg-opacity: 1;
-    background-color: rgb(59 130 246 / var(--tw-bg-opacity));
-  }
-}
diff --git a/tests/svelte-syntax.test.js b/tests/svelte-syntax.test.js
deleted file mode 100644
index 38bf51f6..00000000
--- a/tests/svelte-syntax.test.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-
-import { run, css } from './util/run'
-
-test('basic usage', () => {
-  let config = {
-    content: [path.resolve(__dirname, './svelte-syntax.test.svelte')],
-    corePlugins: { preflight: false },
-    theme: {},
-    plugins: [],
-  }
-
-  let input = css`
-    @tailwind components;
-    @tailwind utilities;
-  `
-
-  return run(input, config).then((result) => {
-    let expectedPath = path.resolve(__dirname, './svelte-syntax.test.css')
-    let expected = fs.readFileSync(expectedPath, 'utf8')
-
-    expect(result.css).toMatchFormattedCss(expected)
-  })
-})
diff --git a/tests/syntax-lit-html.test.js b/tests/syntax-lit-html.test.js
new file mode 100644
index 00000000..dbc8936d
--- /dev/null
+++ b/tests/syntax-lit-html.test.js
@@ -0,0 +1,47 @@
+import { run } from './util/run'
+
+let css = String.raw
+
+test('it detects classes in lit-html templates', () => {
+  let config = {
+    content: [
+      {
+        raw: `html\`<button class="bg-blue-400 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">\${data.title}</button>\`;`,
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .rounded {
+        border-radius: 0.25rem;
+      }
+      .bg-blue-400 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(96 165 250 / var(--tw-bg-opacity));
+      }
+      .py-2 {
+        padding-top: 0.5rem;
+        padding-bottom: 0.5rem;
+      }
+      .px-4 {
+        padding-left: 1rem;
+        padding-right: 1rem;
+      }
+      .font-bold {
+        font-weight: 700;
+      }
+      .text-white {
+        --tw-text-opacity: 1;
+        color: rgb(255 255 255 / var(--tw-text-opacity));
+      }
+      .hover\:bg-blue-600:hover {
+        --tw-bg-opacity: 1;
+        background-color: rgb(37 99 235 / var(--tw-bg-opacity));
+      }
+    `)
+  })
+})
diff --git a/tests/syntax-svelte.test.js b/tests/syntax-svelte.test.js
new file mode 100644
index 00000000..83e038d1
--- /dev/null
+++ b/tests/syntax-svelte.test.js
@@ -0,0 +1,80 @@
+import path from 'path'
+
+import { run } from './util/run'
+
+let css = String.raw
+
+test('it detects svelte based on the file extension', () => {
+  let config = {
+    content: [path.resolve(__dirname, './syntax-svelte.test.svelte')],
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  let input = css`
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .bg-red-500 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(239 68 68 / var(--tw-bg-opacity));
+      }
+      @media (min-width: 1024px) {
+        .lg\:hover\:bg-blue-500:hover {
+          --tw-bg-opacity: 1;
+          background-color: rgb(59 130 246 / var(--tw-bg-opacity));
+        }
+      }
+    `)
+  })
+})
+
+test('using raw with svelte extension', () => {
+  let config = {
+    content: [
+      {
+        raw: `
+        <script>
+          let current = 'foo'
+        </script>
+
+        <button class:lg:hover:bg-blue-500={current === 'foo'}>Click me</button>
+
+        <button
+          class:bg-red-500={current === 'foo'}
+        >
+          Click me
+        </button>
+        `,
+        extension: 'svelte',
+      },
+    ],
+    corePlugins: { preflight: false },
+    theme: {},
+    plugins: [],
+  }
+
+  let input = css`
+    @tailwind components;
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchCss(css`
+      .bg-red-500 {
+        --tw-bg-opacity: 1;
+        background-color: rgb(239 68 68 / var(--tw-bg-opacity));
+      }
+      @media (min-width: 1024px) {
+        .lg\:hover\:bg-blue-500:hover {
+          --tw-bg-opacity: 1;
+          background-color: rgb(59 130 246 / var(--tw-bg-opacity));
+        }
+      }
+    `)
+  })
+})
diff --git a/tests/svelte-syntax.test.svelte b/tests/syntax-svelte.test.svelte
similarity index 100%
rename from tests/svelte-syntax.test.svelte
rename to tests/syntax-svelte.test.svelte
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++])
-  })
-}
diff --git a/tests/variants.test.css b/tests/variants.test.css
index 300d8c79..41e1b08f 100644
--- a/tests/variants.test.css
+++ b/tests/variants.test.css
@@ -52,6 +52,14 @@
   --tw-text-opacity: 1;
   color: rgb(255 255 255 / var(--tw-text-opacity));
 }
+.file\:bg-blue-500::file-selector-button {
+  --tw-bg-opacity: 1;
+  background-color: rgb(59 130 246 / var(--tw-bg-opacity));
+}
+.file\:text-white::file-selector-button {
+  --tw-text-opacity: 1;
+  color: rgb(255 255 255 / var(--tw-text-opacity));
+}
 .before\:block::before {
   content: '';
   display: block;
@@ -189,6 +197,10 @@
   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
     var(--tw-shadow);
 }
+.file\:hover\:bg-blue-600::file-selector-button:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgb(37 99 235 / var(--tw-bg-opacity));
+}
 .focus\:shadow-md:focus {
   --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -1px rgb(0 0 0 / 0.06);
   box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
diff --git a/tests/variants.test.html b/tests/variants.test.html
index 9fb44452..70e1c140 100644
--- a/tests/variants.test.html
+++ b/tests/variants.test.html
@@ -45,6 +45,7 @@
       <li class="marker:text-red-500 marker:text-lg"></li>
     </ul>
     <div class="selection:bg-blue-500 selection:text-white"></div>
+    <div class="file:bg-blue-500 file:text-white"></div>
     <div class="before:block before:bg-red-500"></div>
     <div class="after:flex after:uppercase"></div>
 
@@ -127,6 +128,7 @@
     <div class="2xl:shadow-md"></div>
 
     <!-- Stacked variants -->
+    <div class="file:hover:bg-blue-600"></div>
     <div class="focus:hover:shadow-md"></div>
     <div class="sm:active:shadow-md"></div>
     <div class="md:group-focus:shadow-md"></div>