Repository: tailwindlabs/tailwindcss
Pull request #5387 (2021-09-04 11:28:08)

Add `accent-color` utilities
This PR adds utilities for the [`accent-color`](https://developer.mozilla.org/en-US/docs/Web/CSS/accent-color) property.

Example usage:

`<input type="checkbox" class="accent-red-500">`

lukewarlow: As this only has support in 2/3 browsers, I understand this may not be merged soon.

Here's the accent-color utilities in plugin form: https://github.com/lukewarlow/tailwind-accent-color
lukewarlow: The test is failing locally but the css output doesn't make much sense to me, seeing as this is basically a direct copy of the caret color plugin.

![image](https://user-images.githubusercontent.com/32498324/132093451-79fa2cee-5857-4abd-8c2f-d72d6322c14d.png)


Patch:
diff --git a/.github/workflows/release-insiders.yml b/.github/workflows/release-insiders.yml
new file mode 100644
index 00000000..1b169de6
--- /dev/null
+++ b/.github/workflows/release-insiders.yml
@@ -0,0 +1,58 @@
+name: Release Insiders
+
+on:
+  push:
+    branches: [master]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [12]
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ matrix.node-version }}
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Use cached node_modules
+        id: cache
+        uses: actions/cache@v2
+        with:
+          path: node_modules
+          key: nodeModules-${{ hashFiles('**/package-lock.json') }}-${{ matrix.node-version }}
+          restore-keys: |
+            nodeModules-
+
+      - name: Install dependencies
+        if: steps.cache.outputs.cache-hit != 'true'
+        run: npm install
+        env:
+          CI: true
+
+      - name: Build
+        run: npm run prepublishOnly
+
+      - name: Test
+        run: npm test
+        env:
+          CI: true
+
+      - name: Resolve version
+        id: vars
+        run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
+
+      - name: "Version based on commit: 0.0.0-insiders.${{ steps.vars.outputs.sha_short }}"
+        run: npm version 0.0.0-insiders.${{ steps.vars.outputs.sha_short }} --force --no-git-tag-version
+
+      - name: Publish
+        run: npm publish --tag insiders
+        env:
+          CI: true
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..9264558c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,51 @@
+name: Release
+
+on:
+  release:
+    types: [published]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [12]
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ matrix.node-version }}
+          registry-url: 'https://registry.npmjs.org'
+
+      - name: Use cached node_modules
+        id: cache
+        uses: actions/cache@v2
+        with:
+          path: node_modules
+          key: nodeModules-${{ hashFiles('**/package-lock.json') }}-${{ matrix.node-version }}
+          restore-keys: |
+            nodeModules-
+
+      - name: Install dependencies
+        if: steps.cache.outputs.cache-hit != 'true'
+        run: npm install
+        env:
+          CI: true
+
+      - name: Build
+        run: npm run prepublishOnly
+
+      - name: Test
+        run: npm test
+        env:
+          CI: true
+
+      - name: Publish
+        run: npm publish
+        env:
+          CI: true
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2606b4f2..dba599d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+### Changed
+
+- Remove AOT engine, make JIT the default ([#5340](https://github.com/tailwindlabs/tailwindcss/pull/5340))
+- 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))
+
+### 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))
+
+## [2.2.10] - 2021-09-06
+
+### Fixed
+
+- Fix build error when using `presets: []` in config file ([#4903](https://github.com/tailwindlabs/tailwindcss/pull/4903))
+
+### Added
+
+- Reintroduce universal selector optimizations under experimental `optimizeUniversalDefaults` flag ([a9e160c](https://github.com/tailwindlabs/tailwindcss/commit/a9e160cf9acb75a2bbac34f8864568b12940f89a))
+
+## [2.2.9] - 2021-08-30
+
+### Fixed
+
+- JIT: Fix `@apply`ing utilities that contain variants + the important modifier ([#4854](https://github.com/tailwindlabs/tailwindcss/pull/4854))
+- JIT: Don't strip "null" when parsing tracked file paths ([#5008](https://github.com/tailwindlabs/tailwindcss/pull/5008))
+- Pin `clean-css` to v5.1.4 to fix empty CSS variables in CDN builds ([#5338](https://github.com/tailwindlabs/tailwindcss/pull/5338))
+
+## [2.2.8] - 2021-08-27
+
 ### 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)
 
 ## [2.2.7] - 2021-07-23
 
@@ -1502,7 +1542,10 @@ No release notes
 
 - Everything!
 
-[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.7...HEAD
+[unreleased]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.10...HEAD
+[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
 [2.2.7]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.6...v2.2.7
 [2.2.6]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.5...v2.2.6
 [2.2.5]: https://github.com/tailwindlabs/tailwindcss/compare/v2.2.4...v2.2.5
diff --git a/colors.js b/colors.js
index 4f94e2c2..f8797e77 100644
--- a/colors.js
+++ b/colors.js
@@ -1,146 +1,149 @@
-let warned = false
+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.')
+    warned.push(from)
+  }
+}
 
 module.exports = {
+  transparent: 'transparent',
+  current: 'currentColor',
   black: '#000',
   white: '#fff',
-  rose: {
-    50: '#fff1f2',
-    100: '#ffe4e6',
-    200: '#fecdd3',
-    300: '#fda4af',
-    400: '#fb7185',
-    500: '#f43f5e',
-    600: '#e11d48',
-    700: '#be123c',
-    800: '#9f1239',
-    900: '#881337',
+  slate: {
+    50: '#f8fafc',
+    100: '#f1f5f9',
+    200: '#e2e8f0',
+    300: '#cbd5e1',
+    400: '#94a3b8',
+    500: '#64748b',
+    600: '#475569',
+    700: '#334155',
+    800: '#1e293b',
+    900: '#0f172a',
   },
-  pink: {
-    50: '#fdf2f8',
-    100: '#fce7f3',
-    200: '#fbcfe8',
-    300: '#f9a8d4',
-    400: '#f472b6',
-    500: '#ec4899',
-    600: '#db2777',
-    700: '#be185d',
-    800: '#9d174d',
-    900: '#831843',
+  gray: {
+    50: '#f9fafb',
+    100: '#f3f4f6',
+    200: '#e5e7eb',
+    300: '#d1d5db',
+    400: '#9ca3af',
+    500: '#6b7280',
+    600: '#4b5563',
+    700: '#374151',
+    800: '#1f2937',
+    900: '#111827',
   },
-  fuchsia: {
-    50: '#fdf4ff',
-    100: '#fae8ff',
-    200: '#f5d0fe',
-    300: '#f0abfc',
-    400: '#e879f9',
-    500: '#d946ef',
-    600: '#c026d3',
-    700: '#a21caf',
-    800: '#86198f',
-    900: '#701a75',
+  zinc: {
+    50: '#fafafa',
+    100: '#f4f4f5',
+    200: '#e4e4e7',
+    300: '#d4d4d8',
+    400: '#a1a1aa',
+    500: '#71717a',
+    600: '#52525b',
+    700: '#3f3f46',
+    800: '#27272a',
+    900: '#18181b',
   },
-  purple: {
-    50: '#faf5ff',
-    100: '#f3e8ff',
-    200: '#e9d5ff',
-    300: '#d8b4fe',
-    400: '#c084fc',
-    500: '#a855f7',
-    600: '#9333ea',
-    700: '#7e22ce',
-    800: '#6b21a8',
-    900: '#581c87',
+  neutral: {
+    50: '#fafafa',
+    100: '#f5f5f5',
+    200: '#e5e5e5',
+    300: '#d4d4d4',
+    400: '#a3a3a3',
+    500: '#737373',
+    600: '#525252',
+    700: '#404040',
+    800: '#262626',
+    900: '#171717',
   },
-  violet: {
-    50: '#f5f3ff',
-    100: '#ede9fe',
-    200: '#ddd6fe',
-    300: '#c4b5fd',
-    400: '#a78bfa',
-    500: '#8b5cf6',
-    600: '#7c3aed',
-    700: '#6d28d9',
-    800: '#5b21b6',
-    900: '#4c1d95',
+  stone: {
+    50: '#fafaf9',
+    100: '#f5f5f4',
+    200: '#e7e5e4',
+    300: '#d6d3d1',
+    400: '#a8a29e',
+    500: '#78716c',
+    600: '#57534e',
+    700: '#44403c',
+    800: '#292524',
+    900: '#1c1917',
   },
-  indigo: {
-    50: '#eef2ff',
-    100: '#e0e7ff',
-    200: '#c7d2fe',
-    300: '#a5b4fc',
-    400: '#818cf8',
-    500: '#6366f1',
-    600: '#4f46e5',
-    700: '#4338ca',
-    800: '#3730a3',
-    900: '#312e81',
+  red: {
+    50: '#fef2f2',
+    100: '#fee2e2',
+    200: '#fecaca',
+    300: '#fca5a5',
+    400: '#f87171',
+    500: '#ef4444',
+    600: '#dc2626',
+    700: '#b91c1c',
+    800: '#991b1b',
+    900: '#7f1d1d',
   },
-  blue: {
-    50: '#eff6ff',
-    100: '#dbeafe',
-    200: '#bfdbfe',
-    300: '#93c5fd',
-    400: '#60a5fa',
-    500: '#3b82f6',
-    600: '#2563eb',
-    700: '#1d4ed8',
-    800: '#1e40af',
-    900: '#1e3a8a',
+  orange: {
+    50: '#fff7ed',
+    100: '#ffedd5',
+    200: '#fed7aa',
+    300: '#fdba74',
+    400: '#fb923c',
+    500: '#f97316',
+    600: '#ea580c',
+    700: '#c2410c',
+    800: '#9a3412',
+    900: '#7c2d12',
   },
-  get lightBlue() {
-    if (!warned) {
-      console.log('warn - As of Tailwind CSS v2.2, `lightBlue` has been renamed to `sky`.')
-      console.log('warn - Please update your color palette to eliminate this warning.')
-      warned = true
-    }
-    return {
-      50: '#f0f9ff',
-      100: '#e0f2fe',
-      200: '#bae6fd',
-      300: '#7dd3fc',
-      400: '#38bdf8',
-      500: '#0ea5e9',
-      600: '#0284c7',
-      700: '#0369a1',
-      800: '#075985',
-      900: '#0c4a6e',
-    }
+  amber: {
+    50: '#fffbeb',
+    100: '#fef3c7',
+    200: '#fde68a',
+    300: '#fcd34d',
+    400: '#fbbf24',
+    500: '#f59e0b',
+    600: '#d97706',
+    700: '#b45309',
+    800: '#92400e',
+    900: '#78350f',
   },
-  sky: {
-    50: '#f0f9ff',
-    100: '#e0f2fe',
-    200: '#bae6fd',
-    300: '#7dd3fc',
-    400: '#38bdf8',
-    500: '#0ea5e9',
-    600: '#0284c7',
-    700: '#0369a1',
-    800: '#075985',
-    900: '#0c4a6e',
+  yellow: {
+    50: '#fefce8',
+    100: '#fef9c3',
+    200: '#fef08a',
+    300: '#fde047',
+    400: '#facc15',
+    500: '#eab308',
+    600: '#ca8a04',
+    700: '#a16207',
+    800: '#854d0e',
+    900: '#713f12',
   },
-  cyan: {
-    50: '#ecfeff',
-    100: '#cffafe',
-    200: '#a5f3fc',
-    300: '#67e8f9',
-    400: '#22d3ee',
-    500: '#06b6d4',
-    600: '#0891b2',
-    700: '#0e7490',
-    800: '#155e75',
-    900: '#164e63',
+  lime: {
+    50: '#f7fee7',
+    100: '#ecfccb',
+    200: '#d9f99d',
+    300: '#bef264',
+    400: '#a3e635',
+    500: '#84cc16',
+    600: '#65a30d',
+    700: '#4d7c0f',
+    800: '#3f6212',
+    900: '#365314',
   },
-  teal: {
-    50: '#f0fdfa',
-    100: '#ccfbf1',
-    200: '#99f6e4',
-    300: '#5eead4',
-    400: '#2dd4bf',
-    500: '#14b8a6',
-    600: '#0d9488',
-    700: '#0f766e',
-    800: '#115e59',
-    900: '#134e4a',
+  green: {
+    50: '#f0fdf4',
+    100: '#dcfce7',
+    200: '#bbf7d0',
+    300: '#86efac',
+    400: '#4ade80',
+    500: '#22c55e',
+    600: '#16a34a',
+    700: '#15803d',
+    800: '#166534',
+    900: '#14532d',
   },
   emerald: {
     50: '#ecfdf5',
@@ -154,136 +157,144 @@ module.exports = {
     800: '#065f46',
     900: '#064e3b',
   },
-  green: {
-    50: '#f0fdf4',
-    100: '#dcfce7',
-    200: '#bbf7d0',
-    300: '#86efac',
-    400: '#4ade80',
-    500: '#22c55e',
-    600: '#16a34a',
-    700: '#15803d',
-    800: '#166534',
-    900: '#14532d',
+  teal: {
+    50: '#f0fdfa',
+    100: '#ccfbf1',
+    200: '#99f6e4',
+    300: '#5eead4',
+    400: '#2dd4bf',
+    500: '#14b8a6',
+    600: '#0d9488',
+    700: '#0f766e',
+    800: '#115e59',
+    900: '#134e4a',
   },
-  lime: {
-    50: '#f7fee7',
-    100: '#ecfccb',
-    200: '#d9f99d',
-    300: '#bef264',
-    400: '#a3e635',
-    500: '#84cc16',
-    600: '#65a30d',
-    700: '#4d7c0f',
-    800: '#3f6212',
-    900: '#365314',
+  cyan: {
+    50: '#ecfeff',
+    100: '#cffafe',
+    200: '#a5f3fc',
+    300: '#67e8f9',
+    400: '#22d3ee',
+    500: '#06b6d4',
+    600: '#0891b2',
+    700: '#0e7490',
+    800: '#155e75',
+    900: '#164e63',
   },
-  yellow: {
-    50: '#fefce8',
-    100: '#fef9c3',
-    200: '#fef08a',
-    300: '#fde047',
-    400: '#facc15',
-    500: '#eab308',
-    600: '#ca8a04',
-    700: '#a16207',
-    800: '#854d0e',
-    900: '#713f12',
+  sky: {
+    50: '#f0f9ff',
+    100: '#e0f2fe',
+    200: '#bae6fd',
+    300: '#7dd3fc',
+    400: '#38bdf8',
+    500: '#0ea5e9',
+    600: '#0284c7',
+    700: '#0369a1',
+    800: '#075985',
+    900: '#0c4a6e',
   },
-  amber: {
-    50: '#fffbeb',
-    100: '#fef3c7',
-    200: '#fde68a',
-    300: '#fcd34d',
-    400: '#fbbf24',
-    500: '#f59e0b',
-    600: '#d97706',
-    700: '#b45309',
-    800: '#92400e',
-    900: '#78350f',
+  blue: {
+    50: '#eff6ff',
+    100: '#dbeafe',
+    200: '#bfdbfe',
+    300: '#93c5fd',
+    400: '#60a5fa',
+    500: '#3b82f6',
+    600: '#2563eb',
+    700: '#1d4ed8',
+    800: '#1e40af',
+    900: '#1e3a8a',
   },
-  orange: {
-    50: '#fff7ed',
-    100: '#ffedd5',
-    200: '#fed7aa',
-    300: '#fdba74',
-    400: '#fb923c',
-    500: '#f97316',
-    600: '#ea580c',
-    700: '#c2410c',
-    800: '#9a3412',
-    900: '#7c2d12',
+  indigo: {
+    50: '#eef2ff',
+    100: '#e0e7ff',
+    200: '#c7d2fe',
+    300: '#a5b4fc',
+    400: '#818cf8',
+    500: '#6366f1',
+    600: '#4f46e5',
+    700: '#4338ca',
+    800: '#3730a3',
+    900: '#312e81',
   },
-  red: {
-    50: '#fef2f2',
-    100: '#fee2e2',
-    200: '#fecaca',
-    300: '#fca5a5',
-    400: '#f87171',
-    500: '#ef4444',
-    600: '#dc2626',
-    700: '#b91c1c',
-    800: '#991b1b',
-    900: '#7f1d1d',
+  violet: {
+    50: '#f5f3ff',
+    100: '#ede9fe',
+    200: '#ddd6fe',
+    300: '#c4b5fd',
+    400: '#a78bfa',
+    500: '#8b5cf6',
+    600: '#7c3aed',
+    700: '#6d28d9',
+    800: '#5b21b6',
+    900: '#4c1d95',
   },
-  warmGray: {
-    50: '#fafaf9',
-    100: '#f5f5f4',
-    200: '#e7e5e4',
-    300: '#d6d3d1',
-    400: '#a8a29e',
-    500: '#78716c',
-    600: '#57534e',
-    700: '#44403c',
-    800: '#292524',
-    900: '#1c1917',
+  purple: {
+    50: '#faf5ff',
+    100: '#f3e8ff',
+    200: '#e9d5ff',
+    300: '#d8b4fe',
+    400: '#c084fc',
+    500: '#a855f7',
+    600: '#9333ea',
+    700: '#7e22ce',
+    800: '#6b21a8',
+    900: '#581c87',
   },
-  trueGray: {
-    50: '#fafafa',
-    100: '#f5f5f5',
-    200: '#e5e5e5',
-    300: '#d4d4d4',
-    400: '#a3a3a3',
-    500: '#737373',
-    600: '#525252',
-    700: '#404040',
-    800: '#262626',
-    900: '#171717',
+  fuchsia: {
+    50: '#fdf4ff',
+    100: '#fae8ff',
+    200: '#f5d0fe',
+    300: '#f0abfc',
+    400: '#e879f9',
+    500: '#d946ef',
+    600: '#c026d3',
+    700: '#a21caf',
+    800: '#86198f',
+    900: '#701a75',
   },
-  gray: {
-    50: '#fafafa',
-    100: '#f4f4f5',
-    200: '#e4e4e7',
-    300: '#d4d4d8',
-    400: '#a1a1aa',
-    500: '#71717a',
-    600: '#52525b',
-    700: '#3f3f46',
-    800: '#27272a',
-    900: '#18181b',
+  pink: {
+    50: '#fdf2f8',
+    100: '#fce7f3',
+    200: '#fbcfe8',
+    300: '#f9a8d4',
+    400: '#f472b6',
+    500: '#ec4899',
+    600: '#db2777',
+    700: '#be185d',
+    800: '#9d174d',
+    900: '#831843',
   },
-  coolGray: {
-    50: '#f9fafb',
-    100: '#f3f4f6',
-    200: '#e5e7eb',
-    300: '#d1d5db',
-    400: '#9ca3af',
-    500: '#6b7280',
-    600: '#4b5563',
-    700: '#374151',
-    800: '#1f2937',
-    900: '#111827',
+  rose: {
+    50: '#fff1f2',
+    100: '#ffe4e6',
+    200: '#fecdd3',
+    300: '#fda4af',
+    400: '#fb7185',
+    500: '#f43f5e',
+    600: '#e11d48',
+    700: '#be123c',
+    800: '#9f1239',
+    900: '#881337',
   },
-  blueGray: {
-    50: '#f8fafc',
-    100: '#f1f5f9',
-    200: '#e2e8f0',
-    300: '#cbd5e1',
-    400: '#94a3b8',
-    500: '#64748b',
-    600: '#475569',
-    700: '#334155',
-    800: '#1e293b',
-    900: '#0f172a',
+  get lightBlue() {
+    warn({ version: 'v2.2', from: 'lightBlue', to: 'sky' })
+    return this.sky
+  },
+  get warmGray() {
+    warn({ version: 'v3.0', from: 'warmGray', to: 'stone' })
+    return this.stone
+  },
+  get trueGray() {
+    warn({ version: 'v3.0', from: 'trueGray', to: 'neutral' })
+    return this.neutral
+  },
+  get coolGray() {
+    warn({ version: 'v3.0', from: 'coolGray', to: 'gray' })
+    return this.gray
+  },
+  get blueGray() {
+    warn({ version: 'v3.0', from: 'blueGray', to: 'slate' })
+    return this.slate
   },
 }
diff --git a/defaultConfig.js b/defaultConfig.js
index 820ae3e1..0a131abc 100644
--- a/defaultConfig.js
+++ b/defaultConfig.js
@@ -1,4 +1,4 @@
-const cloneDeep = require('lodash/cloneDeep')
-const defaultConfig = require('./stubs/defaultConfig.stub.js')
+let { cloneDeep } = require('./src/util/cloneDeep')
+let defaultConfig = require('./stubs/defaultConfig.stub.js')
 
 module.exports = cloneDeep(defaultConfig)
diff --git a/defaultTheme.js b/defaultTheme.js
index 70e74af7..4a42a998 100644
--- a/defaultTheme.js
+++ b/defaultTheme.js
@@ -1,4 +1,4 @@
-const cloneDeep = require('lodash/cloneDeep')
-const defaultConfig = require('./stubs/defaultConfig.stub.js')
+let { cloneDeep } = require('./src/util/cloneDeep')
+let defaultConfig = require('./stubs/defaultConfig.stub.js')
 
 module.exports = cloneDeep(defaultConfig.theme)
diff --git a/package-lock.json b/package-lock.json
index 4becce56..16d63944 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,6 @@
         "fast-glob": "^3.2.7",
         "glob-parent": "^6.0.1",
         "is-glob": "^4.0.1",
-        "lodash": "^4.17.21",
         "normalize-path": "^3.0.0",
         "object-hash": "^2.2.0",
         "postcss-js": "^3.0.3",
@@ -29,7 +28,6 @@
         "postcss-selector-parser": "^6.0.6",
         "postcss-value-parser": "^4.1.0",
         "quick-lru": "^5.1.1",
-        "reduce-css-calc": "^2.1.8",
         "resolve": "^1.20.0",
         "tmp": "^0.2.1"
       },
@@ -50,7 +48,7 @@
         "cssnano": "^5.0.8",
         "eslint": "^7.32.0",
         "eslint-config-prettier": "^8.3.0",
-        "eslint-plugin-prettier": "^3.4.1",
+        "eslint-plugin-prettier": "^4.0.0",
         "jest": "^27.1.0",
         "jest-diff": "^27.1.0",
         "postcss": "^8.3.6",
@@ -3669,11 +3667,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/css-unit-converter": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
-      "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA=="
-    },
     "node_modules/css-what": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
@@ -4356,9 +4349,9 @@
       }
     },
     "node_modules/eslint-plugin-prettier": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz",
-      "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz",
+      "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==",
       "dev": true,
       "dependencies": {
         "prettier-linter-helpers": "^1.0.0"
@@ -4367,8 +4360,8 @@
         "node": ">=6.0.0"
       },
       "peerDependencies": {
-        "eslint": ">=5.0.0",
-        "prettier": ">=1.13.0"
+        "eslint": ">=7.28.0",
+        "prettier": ">=2.0.0"
       },
       "peerDependenciesMeta": {
         "eslint-config-prettier": {
@@ -7031,7 +7024,8 @@
     "node_modules/lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
     },
     "node_modules/lodash.clonedeep": {
       "version": "4.5.0",
@@ -8732,20 +8726,6 @@
         "node": ">=0.10"
       }
     },
-    "node_modules/reduce-css-calc": {
-      "version": "2.1.8",
-      "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
-      "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
-      "dependencies": {
-        "css-unit-converter": "^1.1.1",
-        "postcss-value-parser": "^3.3.0"
-      }
-    },
-    "node_modules/reduce-css-calc/node_modules/postcss-value-parser": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-      "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
-    },
     "node_modules/regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -13078,11 +13058,6 @@
         }
       }
     },
-    "css-unit-converter": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
-      "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA=="
-    },
     "css-what": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz",
@@ -13640,9 +13615,9 @@
       "requires": {}
     },
     "eslint-plugin-prettier": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz",
-      "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz",
+      "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==",
       "dev": true,
       "requires": {
         "prettier-linter-helpers": "^1.0.0"
@@ -15617,7 +15592,8 @@
     "lodash": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
     },
     "lodash.clonedeep": {
       "version": "4.5.0",
@@ -16841,22 +16817,6 @@
         "readable-stream": "^2.0.2"
       }
     },
-    "reduce-css-calc": {
-      "version": "2.1.8",
-      "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
-      "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
-      "requires": {
-        "css-unit-converter": "^1.1.1",
-        "postcss-value-parser": "^3.3.0"
-      },
-      "dependencies": {
-        "postcss-value-parser": {
-          "version": "3.3.1",
-          "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-          "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
-        }
-      }
-    },
     "regenerate": {
       "version": "1.4.2",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
diff --git a/package.json b/package.json
index 218d320b..b647d08c 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
     "generate:plugin-list": "babel-node scripts/create-plugin-list.js"
   },
   "files": [
-    "dist/*.css",
+    "src/*",
     "cli/*",
     "lib/*",
     "peers/*",
@@ -54,7 +54,7 @@
     "cssnano": "^5.0.8",
     "eslint": "^7.32.0",
     "eslint-config-prettier": "^8.3.0",
-    "eslint-plugin-prettier": "^3.4.1",
+    "eslint-plugin-prettier": "^4.0.0",
     "jest": "^27.1.0",
     "jest-diff": "^27.1.0",
     "postcss": "^8.3.6",
@@ -78,7 +78,6 @@
     "fast-glob": "^3.2.7",
     "glob-parent": "^6.0.1",
     "is-glob": "^4.0.1",
-    "lodash": "^4.17.21",
     "normalize-path": "^3.0.0",
     "object-hash": "^2.2.0",
     "postcss-js": "^3.0.3",
@@ -87,7 +86,6 @@
     "postcss-selector-parser": "^6.0.6",
     "postcss-value-parser": "^4.1.0",
     "quick-lru": "^5.1.1",
-    "reduce-css-calc": "^2.1.8",
     "resolve": "^1.20.0",
     "tmp": "^0.2.1"
   },
diff --git a/src/featureFlags.js b/src/featureFlags.js
index 520c8797..957c079c 100644
--- a/src/featureFlags.js
+++ b/src/featureFlags.js
@@ -1,19 +1,18 @@
-import _ from 'lodash'
 import chalk from 'chalk'
 import log from './util/log'
 
 const featureFlags = {
   future: [],
-  experimental: [],
+  experimental: ['optimizeUniversalDefaults'],
 }
 
 export function flagEnabled(config, flag) {
   if (featureFlags.future.includes(flag)) {
-    return config.future === 'all' || _.get(config, ['future', flag], false)
+    return config.future === 'all' || (config?.future?.[flag] ?? false)
   }
 
   if (featureFlags.experimental.includes(flag)) {
-    return config.experimental === 'all' || _.get(config, ['experimental', flag], false)
+    return config.experimental === 'all' || (config?.experimental?.[flag] ?? false)
   }
 
   return false
@@ -24,7 +23,7 @@ function experimentalFlagsEnabled(config) {
     return featureFlags.experimental
   }
 
-  return Object.keys(_.get(config, 'experimental', {})).filter(
+  return Object.keys(config?.experimental ?? {}).filter(
     (flag) => featureFlags.experimental.includes(flag) && config.experimental[flag]
   )
 }
diff --git a/src/lib/evaluateTailwindFunctions.js b/src/lib/evaluateTailwindFunctions.js
index 0e2faa0f..fb3474b2 100644
--- a/src/lib/evaluateTailwindFunctions.js
+++ b/src/lib/evaluateTailwindFunctions.js
@@ -1,15 +1,20 @@
-import _ from 'lodash'
+import dlv from 'dlv'
 import didYouMean from 'didyoumean'
 import transformThemeValue from '../util/transformThemeValue'
 import parseValue from 'postcss-value-parser'
 import buildMediaQuery from '../util/buildMediaQuery'
+import { toPath } from '../util/toPath'
+
+function isObject(input) {
+  return typeof input === 'object' && input !== null
+}
 
 function findClosestExistingPath(theme, path) {
-  const parts = _.toPath(path)
+  let parts = toPath(path)
   do {
     parts.pop()
 
-    if (_.hasIn(theme, parts)) break
+    if (dlv(theme, parts) !== undefined) break
   } while (parts.length)
 
   return parts.length ? parts : undefined
@@ -32,20 +37,22 @@ function listKeys(obj) {
 }
 
 function validatePath(config, path, defaultValue) {
-  const pathString = Array.isArray(path) ? pathToString(path) : _.trim(path, `'"`)
-  const pathSegments = Array.isArray(path) ? path : _.toPath(pathString)
-  const value = _.get(config.theme, pathString, defaultValue)
+  const pathString = Array.isArray(path)
+    ? pathToString(path)
+    : path.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
+  const pathSegments = Array.isArray(path) ? path : toPath(pathString)
+  const value = dlv(config.theme, pathString, defaultValue)
 
-  if (typeof value === 'undefined') {
+  if (value === undefined) {
     let error = `'${pathString}' does not exist in your theme config.`
     const parentSegments = pathSegments.slice(0, -1)
-    const parentValue = _.get(config.theme, parentSegments)
+    const parentValue = dlv(config.theme, parentSegments)
 
-    if (_.isObject(parentValue)) {
+    if (isObject(parentValue)) {
       const validKeys = Object.keys(parentValue).filter(
         (key) => validatePath(config, [...parentSegments, key]).isValid
       )
-      const suggestion = didYouMean(_.last(pathSegments), validKeys)
+      const suggestion = didYouMean(pathSegments[pathSegments.length - 1], validKeys)
       if (suggestion) {
         error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?`
       } else if (validKeys.length > 0) {
@@ -56,8 +63,8 @@ function validatePath(config, path, defaultValue) {
     } else {
       const closestPath = findClosestExistingPath(config.theme, pathString)
       if (closestPath) {
-        const closestValue = _.get(config.theme, closestPath)
-        if (_.isObject(closestValue)) {
+        const closestValue = dlv(config.theme, closestPath)
+        if (isObject(closestValue)) {
           error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys(
             closestValue
           )}`
@@ -87,7 +94,7 @@ function validatePath(config, path, defaultValue) {
   ) {
     let error = `'${pathString}' was found but does not resolve to a string.`
 
-    if (_.isObject(value)) {
+    if (isObject(value)) {
       let validKeys = Object.keys(value).filter(
         (key) => validatePath(config, [...pathSegments, key]).isValid
       )
@@ -165,7 +172,7 @@ export default function ({ tailwindConfig: config }) {
       return value
     },
     screen: (node, screen) => {
-      screen = _.trim(screen, `'"`)
+      screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
 
       if (config.theme.screens[screen] === undefined) {
         throw node.error(`The '${screen}' screen does not exist in your theme.`)
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
index e9de7713..ed3caa13 100644
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -97,6 +97,7 @@ function processApply(root, context) {
     for (let util of candidates) {
       applyCandidates.add(util)
     }
+
     applies.push(rule)
   })
 
@@ -210,7 +211,12 @@ function processApply(root, context) {
             })
           }
 
-          siblings.push([meta, root.nodes[0]])
+          // Insert it
+          siblings.push([
+            // Ensure that when we are sorting, that we take the layer order into account
+            { ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
+            root.nodes[0],
+          ])
         }
       }
 
diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js
index 747ae145..c046bea8 100644
--- a/src/lib/expandTailwindAtRules.js
+++ b/src/lib/expandTailwindAtRules.js
@@ -109,6 +109,16 @@ function buildStylesheet(rules, context) {
     components: new Set(),
     utilities: new Set(),
     variants: new Set(),
+
+    // All the CSS that is not Tailwind related can be put in this bucket. This
+    // will make it easier to later use this information when we want to
+    // `@apply` for example. The main reason we do this here is because we
+    // still need to make sure the order is correct. Last but not least, we
+    // will make sure to always re-inject this section into the css, even if
+    // certain rules were not used. This means that it will look like a no-op
+    // from the user's perspective, but we gathered all the useful information
+    // we need.
+    user: new Set(),
   }
 
   for (let [sort, rule] of sortedRules) {
@@ -131,6 +141,11 @@ function buildStylesheet(rules, context) {
       returnValue.utilities.add(rule)
       continue
     }
+
+    if (sort & context.layerOrder.user) {
+      returnValue.user.add(rule)
+      continue
+    }
   }
 
   return returnValue
diff --git a/src/lib/resolveDefaultsAtRules.js b/src/lib/resolveDefaultsAtRules.js
index 0727aa9e..95887b35 100644
--- a/src/lib/resolveDefaultsAtRules.js
+++ b/src/lib/resolveDefaultsAtRules.js
@@ -1,5 +1,6 @@
 import postcss from 'postcss'
 import selectorParser from 'postcss-selector-parser'
+import { flagEnabled } from '../featureFlags'
 
 function minimumImpactSelector(nodes) {
   let pseudos = nodes.filter((n) => n.type === 'pseudo')
@@ -51,7 +52,7 @@ function extractElementSelector(selector) {
   return cache.get(selector)
 }
 
-export default function resolveDefaultsAtRules() {
+export default function resolveDefaultsAtRules({ tailwindConfig }) {
   return (root) => {
     let variableNodeMap = new Map()
     let universals = new Set()
@@ -90,9 +91,11 @@ export default function resolveDefaultsAtRules() {
 
       let universalRule = postcss.rule()
 
-      // TODO: Fix this, this is a hotfix
-      // universalRule.selectors = [...selectors]
-      universalRule.selectors = ['*', '::before', '::after']
+      if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
+        universalRule.selectors = [...selectors]
+      } else {
+        universalRule.selectors = ['*', '::before', '::after']
+      }
 
       universalRule.append(universal.nodes)
       universal.before(universalRule)
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index 9c6b3325..9c37f968 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -15,44 +15,7 @@ import bigSign from '../util/bigSign'
 import corePlugins from '../corePlugins'
 import * as sharedState from './sharedState'
 import { env } from './sharedState'
-
-function toPath(value) {
-  if (Array.isArray(value)) {
-    return value
-  }
-
-  let inBrackets = false
-  let parts = []
-  let chunk = ''
-
-  for (let i = 0; i < value.length; i++) {
-    let char = value[i]
-    if (char === '[') {
-      inBrackets = true
-      parts.push(chunk)
-      chunk = ''
-      continue
-    }
-    if (char === ']' && inBrackets) {
-      inBrackets = false
-      parts.push(chunk)
-      chunk = ''
-      continue
-    }
-    if (char === '.' && !inBrackets && chunk.length > 0) {
-      parts.push(chunk)
-      chunk = ''
-      continue
-    }
-    chunk = chunk + char
-  }
-
-  if (chunk.length > 0) {
-    parts.push(chunk)
-  }
-
-  return parts
-}
+import { toPath } from '../util/toPath'
 
 function insertInto(list, value, { before = [] } = {}) {
   before = [].concat(before)
@@ -238,6 +201,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
       return getConfigValue(['variants', path], defaultValue)
     },
+    addUserCss(userCss) {
+      for (let [identifier, rule] of withIdentifiers(userCss)) {
+        let offset = offsets.user++
+
+        if (!context.candidateRuleMap.has(identifier)) {
+          context.candidateRuleMap.set(identifier, [])
+        }
+
+        context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
+      }
+    },
     addBase(base) {
       for (let [identifier, rule] of withIdentifiers(base)) {
         let prefixedIdentifier = prefixIdentifier(identifier, {})
@@ -441,6 +415,15 @@ function collectLayerPlugins(root) {
     }
   })
 
+  root.walkRules((rule) => {
+    // At this point it is safe to include all the left-over css from the
+    // user's css file. This is because the `@tailwind` and `@layer` directives
+    // will already be handled and will be removed from the css tree.
+    layerPlugins.push(function ({ addUserCss }) {
+      addUserCss(rule, { respectPrefix: false })
+    })
+  })
+
   return layerPlugins
 }
 
@@ -485,6 +468,7 @@ function registerPlugins(plugins, context) {
     base: 0n,
     components: 0n,
     utilities: 0n,
+    user: 0n,
   }
 
   let pluginApi = buildPluginApi(context.tailwindConfig, context, {
@@ -499,7 +483,7 @@ function registerPlugins(plugins, context) {
         pluginItem(pluginApi)
       }
     } else {
-      plugin(pluginApi)
+      plugin?.(pluginApi)
     }
   }
 
@@ -507,6 +491,7 @@ function registerPlugins(plugins, context) {
     offsets.base,
     offsets.components,
     offsets.utilities,
+    offsets.user,
   ])
   let reservedBits = BigInt(highestOffset.toString(2).length)
 
@@ -514,9 +499,10 @@ function registerPlugins(plugins, context) {
     base: (1n << reservedBits) << 0n,
     components: (1n << reservedBits) << 1n,
     utilities: (1n << reservedBits) << 2n,
+    user: (1n << reservedBits) << 3n,
   }
 
-  reservedBits += 3n
+  reservedBits += 4n
 
   let offset = 0
   context.variantOrder = new Map(
diff --git a/src/lib/substituteResponsiveAtRules.js b/src/lib/substituteResponsiveAtRules.js
index 3147fc56..d15f2cea 100644
--- a/src/lib/substituteResponsiveAtRules.js
+++ b/src/lib/substituteResponsiveAtRules.js
@@ -1,8 +1,8 @@
-import _ from 'lodash'
 import postcss from 'postcss'
 import cloneNodes from '../util/cloneNodes'
 import buildMediaQuery from '../util/buildMediaQuery'
 import buildSelectorVariant from '../util/buildSelectorVariant'
+import { tap } from '../util/tap'
 
 function isLayer(node) {
   if (Array.isArray(node)) {
@@ -20,9 +20,9 @@ export default function (config) {
     // Wrap any `responsive` rules with a copy of their parent `layer` to
     // ensure the layer isn't lost when copying to the `screens` location.
     css.walkAtRules('layer', (layerAtRule) => {
-      const layer = layerAtRule.params
+      let layer = layerAtRule.params
       layerAtRule.walkAtRules('responsive', (responsiveAtRule) => {
-        const nestedlayerAtRule = postcss.atRule({
+        let nestedlayerAtRule = postcss.atRule({
           name: 'layer',
           params: layer,
         })
@@ -32,15 +32,15 @@ export default function (config) {
       })
     })
 
-    const {
+    let {
       theme: { screens },
       separator,
     } = config
-    const responsiveRules = postcss.root()
-    const finalRules = []
+    let responsiveRules = postcss.root()
+    let finalRules = []
 
     css.walkAtRules('responsive', (atRule) => {
-      const nodes = atRule.nodes
+      let nodes = atRule.nodes
       responsiveRules.append(...cloneNodes(nodes))
 
       // If the parent is already a `layer` (this is true for anything coming from
@@ -55,16 +55,16 @@ export default function (config) {
       atRule.remove()
     })
 
-    _.keys(screens).forEach((screen) => {
-      const mediaQuery = postcss.atRule({
+    for (let [screen, value] of Object.entries(screens ?? {})) {
+      let mediaQuery = postcss.atRule({
         name: 'media',
-        params: buildMediaQuery(screens[screen]),
+        params: buildMediaQuery(value),
       })
 
       mediaQuery.append(
-        _.tap(responsiveRules.clone(), (clonedRoot) => {
+        tap(responsiveRules.clone(), (clonedRoot) => {
           clonedRoot.walkRules((rule) => {
-            rule.selectors = _.map(rule.selectors, (selector) =>
+            rule.selectors = rule.selectors.map((selector) =>
               buildSelectorVariant(selector, screen, separator, (message) => {
                 throw rule.error(message)
               })
@@ -74,9 +74,9 @@ export default function (config) {
       )
 
       finalRules.push(mediaQuery)
-    })
+    }
 
-    const hasScreenRules = finalRules.some((i) => i.nodes.length !== 0)
+    let hasScreenRules = finalRules.some((i) => i.nodes.length !== 0)
 
     css.walkAtRules('tailwind', (atRule) => {
       if (atRule.params !== 'screens') {
diff --git a/src/lib/substituteScreenAtRules.js b/src/lib/substituteScreenAtRules.js
index 84c2fdde..1b61d38f 100644
--- a/src/lib/substituteScreenAtRules.js
+++ b/src/lib/substituteScreenAtRules.js
@@ -1,4 +1,3 @@
-import _ from 'lodash'
 import buildMediaQuery from '../util/buildMediaQuery'
 
 export default function ({ tailwindConfig: { theme } }) {
@@ -6,7 +5,7 @@ export default function ({ tailwindConfig: { theme } }) {
     css.walkAtRules('screen', (atRule) => {
       const screen = atRule.params
 
-      if (!_.has(theme.screens, screen)) {
+      if (!theme.screens?.hasOwnProperty?.(screen)) {
         throw atRule.error(`No \`${screen}\` screen found.`)
       }
 
diff --git a/src/plugins/accentColor.js b/src/plugins/accentColor.js
new file mode 100644
index 00000000..de8c5958
--- /dev/null
+++ b/src/plugins/accentColor.js
@@ -0,0 +1,19 @@
+import flattenColorPalette from '../util/flattenColorPalette'
+import toColorValue from '../util/toColorValue'
+
+export default function () {
+  return function ({ matchUtilities, theme, variants }) {
+    matchUtilities(
+      {
+        accent: (value) => {
+          return { 'accent-color': toColorValue(value) }
+        },
+      },
+      {
+        values: flattenColorPalette(theme('accentColor')),
+        variants: variants('accentColor'),
+        type: 'color',
+      }
+    )
+  }
+}
diff --git a/src/plugins/container.js b/src/plugins/container.js
index 21e24cd1..4093750f 100644
--- a/src/plugins/container.js
+++ b/src/plugins/container.js
@@ -1,9 +1,6 @@
-/* eslint-disable no-shadow */
-import _ from 'lodash'
-
 function extractMinWidths(breakpoints) {
-  return _.flatMap(breakpoints, (breakpoints) => {
-    if (_.isString(breakpoints)) {
+  return Object.values(breakpoints ?? {}).flatMap((breakpoints) => {
+    if (typeof breakpoints === 'string') {
       breakpoints = { min: breakpoints }
     }
 
@@ -11,14 +8,13 @@ function extractMinWidths(breakpoints) {
       breakpoints = [breakpoints]
     }
 
-    return _(breakpoints)
+    return breakpoints
       .filter((breakpoint) => {
-        return _.has(breakpoint, 'min') || _.has(breakpoint, 'min-width')
+        return breakpoint?.hasOwnProperty?.('min') || breakpoint?.hasOwnProperty('min-width')
       })
       .map((breakpoint) => {
-        return _.get(breakpoint, 'min-width', breakpoint.min)
+        return breakpoint['min-width'] ?? breakpoint.min
       })
-      .value()
   })
 }
 
@@ -27,7 +23,7 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
     return []
   }
 
-  if (!_.isObject(paddings)) {
+  if (!(typeof paddings === 'object' && paddings !== null)) {
     return [
       {
         screen: 'DEFAULT',
@@ -37,7 +33,7 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
     ]
   }
 
-  const mapping = []
+  let mapping = []
 
   if (paddings.DEFAULT) {
     mapping.push({
@@ -47,11 +43,10 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
     })
   }
 
-  _.each(minWidths, (minWidth) => {
-    Object.keys(screens).forEach((screen) => {
-      const screenMinWidth = _.isPlainObject(screens[screen])
-        ? screens[screen].min || screens[screen]['min-width']
-        : screens[screen]
+  for (let minWidth of minWidths) {
+    for (let [screen, value] of Object.entries(screens)) {
+      let screenMinWidth =
+        typeof value === 'object' && value !== null ? value.min || value['min-width'] : value
 
       if (`${screenMinWidth}` === `${minWidth}`) {
         mapping.push({
@@ -60,20 +55,20 @@ function mapMinWidthsToPadding(minWidths, screens, paddings) {
           padding: paddings[screen],
         })
       }
-    })
-  })
+    }
+  }
 
   return mapping
 }
 
 module.exports = function () {
   return function ({ addComponents, theme, variants }) {
-    const screens = theme('container.screens', theme('screens'))
-    const minWidths = extractMinWidths(screens)
-    const paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
+    let screens = theme('container.screens', theme('screens'))
+    let minWidths = extractMinWidths(screens)
+    let paddings = mapMinWidthsToPadding(minWidths, screens, theme('container.padding'))
 
-    const generatePaddingFor = (minWidth) => {
-      const paddingConfig = _.find(paddings, (padding) => `${padding.minWidth}` === `${minWidth}`)
+    let generatePaddingFor = (minWidth) => {
+      let paddingConfig = paddings.find((padding) => `${padding.minWidth}` === `${minWidth}`)
 
       if (!paddingConfig) {
         return {}
@@ -85,20 +80,16 @@ module.exports = function () {
       }
     }
 
-    const atRules = _(minWidths)
-      .sortBy((minWidth) => parseInt(minWidth))
-      .sortedUniq()
-      .map((minWidth) => {
-        return {
-          [`@media (min-width: ${minWidth})`]: {
-            '.container': {
-              'max-width': minWidth,
-              ...generatePaddingFor(minWidth),
-            },
-          },
-        }
-      })
-      .value()
+    let atRules = Array.from(
+      new Set(minWidths.slice().sort((a, z) => parseInt(a) - parseInt(z)))
+    ).map((minWidth) => ({
+      [`@media (min-width: ${minWidth})`]: {
+        '.container': {
+          'max-width': minWidth,
+          ...generatePaddingFor(minWidth),
+        },
+      },
+    }))
 
     addComponents(
       [
diff --git a/src/plugins/dropShadow.js b/src/plugins/dropShadow.js
index 0d47693e..0711dda8 100644
--- a/src/plugins/dropShadow.js
+++ b/src/plugins/dropShadow.js
@@ -1,10 +1,9 @@
-import _ from 'lodash'
 import nameClass from '../util/nameClass'
 
 export default function () {
   return function ({ addUtilities, theme, variants }) {
-    const utilities = _.fromPairs(
-      _.map(theme('dropShadow'), (value, modifier) => {
+    let utilities = Object.fromEntries(
+      Object.entries(theme('dropShadow') ?? {}).map(([modifier, value]) => {
         return [
           nameClass('drop-shadow', modifier),
           {
diff --git a/src/plugins/index.js b/src/plugins/index.js
index 72fbf41b..4407c24d 100644
--- a/src/plugins/index.js
+++ b/src/plugins/index.js
@@ -125,6 +125,7 @@ export { default as fontSmoothing } from './fontSmoothing'
 export { default as placeholderColor } from './placeholderColor'
 export { default as placeholderOpacity } from './placeholderOpacity'
 export { default as caretColor } from './caretColor'
+export { default as accentColor } from './accentColor'
 
 export { default as opacity } from './opacity'
 export { default as backgroundBlendMode } from './backgroundBlendMode'
diff --git a/src/util/buildMediaQuery.js b/src/util/buildMediaQuery.js
index bb42c9db..ba3b696e 100644
--- a/src/util/buildMediaQuery.js
+++ b/src/util/buildMediaQuery.js
@@ -1,7 +1,5 @@
-import _ from 'lodash'
-
 export default function buildMediaQuery(screens) {
-  if (_.isString(screens)) {
+  if (typeof screens === 'string') {
     screens = { min: screens }
   }
 
@@ -9,22 +7,15 @@ export default function buildMediaQuery(screens) {
     screens = [screens]
   }
 
-  return _(screens)
+  return screens
     .map((screen) => {
-      if (_.has(screen, 'raw')) {
+      if (screen?.hasOwnProperty?.('raw')) {
         return screen.raw
       }
 
-      return _(screen)
-        .map((value, feature) => {
-          feature = _.get(
-            {
-              min: 'min-width',
-              max: 'max-width',
-            },
-            feature,
-            feature
-          )
+      return Object.entries(screen)
+        .map(([feature, value]) => {
+          feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
           return `(${feature}: ${value})`
         })
         .join(' and ')
diff --git a/src/util/buildSelectorVariant.js b/src/util/buildSelectorVariant.js
index dbb291d6..542c5ebd 100644
--- a/src/util/buildSelectorVariant.js
+++ b/src/util/buildSelectorVariant.js
@@ -1,5 +1,5 @@
 import parser from 'postcss-selector-parser'
-import tap from 'lodash/tap'
+import { tap } from './tap'
 import { useMemo } from './useMemo'
 
 const buildSelectorVariant = useMemo(
diff --git a/src/util/cloneDeep.js b/src/util/cloneDeep.js
new file mode 100644
index 00000000..47a12176
--- /dev/null
+++ b/src/util/cloneDeep.js
@@ -0,0 +1,11 @@
+export function cloneDeep(value) {
+  if (Array.isArray(value)) {
+    return value.map((child) => cloneDeep(child))
+  }
+
+  if (typeof value === 'object' && value !== null) {
+    return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)]))
+  }
+
+  return value
+}
diff --git a/src/util/defaults.js b/src/util/defaults.js
new file mode 100644
index 00000000..709387a6
--- /dev/null
+++ b/src/util/defaults.js
@@ -0,0 +1,11 @@
+export function defaults(target, ...sources) {
+  for (let source of sources) {
+    for (let k in source) {
+      if (!target?.hasOwnProperty?.(k)) {
+        target[k] = source[k]
+      }
+    }
+  }
+
+  return target
+}
diff --git a/src/util/escapeClassName.js b/src/util/escapeClassName.js
index 2469c37a..cb5924a9 100644
--- a/src/util/escapeClassName.js
+++ b/src/util/escapeClassName.js
@@ -1,9 +1,8 @@
 import parser from 'postcss-selector-parser'
-import get from 'lodash/get'
 import escapeCommas from './escapeCommas'
 
 export default function escapeClassName(className) {
-  const node = parser.className()
+  let node = parser.className()
   node.value = className
-  return escapeCommas(get(node, 'raws.value', node.value))
+  return escapeCommas(node?.raws?.value ?? node.value)
 }
diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js
index 2bd17cbe..c3b426d1 100644
--- a/src/util/generateVariantFunction.js
+++ b/src/util/generateVariantFunction.js
@@ -1,13 +1,12 @@
-import _ from 'lodash'
 import postcss from 'postcss'
 import selectorParser from 'postcss-selector-parser'
 import { useMemo } from './useMemo'
 
-const classNameParser = selectorParser((selectors) => {
+let classNameParser = selectorParser((selectors) => {
   return selectors.first.filter(({ type }) => type === 'class').pop().value
 })
 
-const getClassNameFromSelector = useMemo(
+let getClassNameFromSelector = useMemo(
   (selector) => classNameParser.transformSync(selector),
   (selector) => selector
 )
@@ -16,10 +15,10 @@ export default function generateVariantFunction(generator, options = {}) {
   return {
     options,
     handler: (container, config) => {
-      const cloned = postcss.root({ nodes: container.clone().nodes })
+      let cloned = postcss.root({ nodes: container.clone().nodes })
 
       container.before(
-        _.defaultTo(
+        (
           generator({
             container: cloned,
             separator: config.separator,
@@ -40,8 +39,7 @@ export default function generateVariantFunction(generator, options = {}) {
               })
               return cloned
             },
-          }),
-          cloned
+          }) ?? cloned
         ).nodes
       )
     },
diff --git a/src/util/negateValue.js b/src/util/negateValue.js
index ad0ac8db..da7a5cf2 100644
--- a/src/util/negateValue.js
+++ b/src/util/negateValue.js
@@ -1,9 +1,14 @@
-import reduceCalc from 'reduce-css-calc'
-
 export default function (value) {
-  try {
-    return reduceCalc(`calc(${value} * -1)`)
-  } catch (e) {
-    return value
+  value = `${value}`
+
+  // Flip sign of numbers
+  if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) {
+    return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
   }
+
+  if (value.includes('var(') || value.includes('calc(')) {
+    return `calc(${value} * -1)`
+  }
+
+  return value
 }
diff --git a/src/util/parseObjectStyles.js b/src/util/parseObjectStyles.js
index f22034eb..cb54787d 100644
--- a/src/util/parseObjectStyles.js
+++ b/src/util/parseObjectStyles.js
@@ -1,4 +1,3 @@
-import _ from 'lodash'
 import postcss from 'postcss'
 import postcssNested from 'postcss-nested'
 import postcssJs from 'postcss-js'
@@ -8,7 +7,7 @@ export default function parseObjectStyles(styles) {
     return parseObjectStyles([styles])
   }
 
-  return _.flatMap(styles, (style) => {
+  return styles.flatMap((style) => {
     return postcss([
       postcssNested({
         bubble: ['screen'],
diff --git a/src/util/prefixNegativeModifiers.js b/src/util/prefixNegativeModifiers.js
index 1642b155..70cefa4c 100644
--- a/src/util/prefixNegativeModifiers.js
+++ b/src/util/prefixNegativeModifiers.js
@@ -1,9 +1,7 @@
-import _ from 'lodash'
-
 export default function prefixNegativeModifiers(base, modifier) {
   if (modifier === '-') {
     return `-${base}`
-  } else if (_.startsWith(modifier, '-')) {
+  } else if (modifier.startsWith('-')) {
     return `-${base}-${modifier.slice(1)}`
   } else {
     return `${base}-${modifier}`
diff --git a/src/util/prefixSelector.js b/src/util/prefixSelector.js
index 3b61f31d..4816b681 100644
--- a/src/util/prefixSelector.js
+++ b/src/util/prefixSelector.js
@@ -1,5 +1,5 @@
 import parser from 'postcss-selector-parser'
-import tap from 'lodash/tap'
+import { tap } from './tap'
 
 export default function (prefix, selector) {
   const getPrefix =
diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js
index bbe3014d..5f544159 100644
--- a/src/util/processPlugins.js
+++ b/src/util/processPlugins.js
@@ -1,31 +1,29 @@
-import _ from 'lodash'
+import dlv from 'dlv'
 import postcss from 'postcss'
 import Node from 'postcss/lib/node'
-import isFunction from 'lodash/isFunction'
-import escapeClassName from '../util/escapeClassName'
-import generateVariantFunction from '../util/generateVariantFunction'
-import parseObjectStyles from '../util/parseObjectStyles'
-import prefixSelector from '../util/prefixSelector'
-import wrapWithVariants from '../util/wrapWithVariants'
-import cloneNodes from '../util/cloneNodes'
+import escapeClassName from './escapeClassName'
+import generateVariantFunction from './generateVariantFunction'
+import parseObjectStyles from './parseObjectStyles'
+import prefixSelector from './prefixSelector'
+import wrapWithVariants from './wrapWithVariants'
+import cloneNodes from './cloneNodes'
 import transformThemeValue from './transformThemeValue'
-import nameClass from '../util/nameClass'
-import isKeyframeRule from '../util/isKeyframeRule'
+import nameClass from './nameClass'
+import isKeyframeRule from './isKeyframeRule'
+import { toPath } from './toPath'
+import { defaults } from './defaults'
 
 function parseStyles(styles) {
   if (!Array.isArray(styles)) {
     return parseStyles([styles])
   }
 
-  return _.flatMap(styles, (style) => (style instanceof Node ? style : parseObjectStyles(style)))
+  return styles.flatMap((style) => (style instanceof Node ? style : parseObjectStyles(style)))
 }
 
 function wrapWithLayer(rules, layer) {
   return postcss
-    .atRule({
-      name: 'layer',
-      params: layer,
-    })
+    .atRule({ name: 'layer', params: layer })
     .append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
 }
 
@@ -48,7 +46,7 @@ export default function (plugins, config) {
 
     options = Array.isArray(options)
       ? Object.assign({}, defaultOptions, { variants: options })
-      : _.defaults(options, defaultOptions)
+      : defaults(options, defaultOptions)
 
     const styles = postcss.root({ nodes: parseStyles(utilities) })
 
@@ -70,21 +68,21 @@ export default function (plugins, config) {
     )
   }
 
-  const getConfigValue = (path, defaultValue) => (path ? _.get(config, path, defaultValue) : config)
+  const getConfigValue = (path, defaultValue) => (path ? dlv(config, path, defaultValue) : config)
 
   plugins.forEach((plugin) => {
     if (plugin.__isOptionsFunction) {
       plugin = plugin()
     }
 
-    const handler = isFunction(plugin) ? plugin : _.get(plugin, 'handler', () => {})
+    const handler = typeof plugin === 'function' ? plugin : plugin?.handler ?? (() => {})
 
     handler({
       postcss,
       config: getConfigValue,
       theme: (path, defaultValue) => {
-        const [pathRoot, ...subPaths] = _.toPath(path)
-        const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
+        let [pathRoot, ...subPaths] = toPath(path)
+        let value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue)
 
         return transformThemeValue(pathRoot)(value)
       },
@@ -135,7 +133,7 @@ export default function (plugins, config) {
 
         options = Array.isArray(options)
           ? Object.assign({}, defaultOptions, { variants: options })
-          : _.defaults(options, defaultOptions)
+          : defaults(options, defaultOptions)
 
         const styles = postcss.root({ nodes: parseStyles(components) })
 
diff --git a/src/util/resolveConfig.js b/src/util/resolveConfig.js
index c031b0ee..3fae7426 100644
--- a/src/util/resolveConfig.js
+++ b/src/util/resolveConfig.js
@@ -1,20 +1,46 @@
-import some from 'lodash/some'
-import mergeWith from 'lodash/mergeWith'
-import isFunction from 'lodash/isFunction'
-import isUndefined from 'lodash/isUndefined'
-import defaults from 'lodash/defaults'
-import map from 'lodash/map'
-import get from 'lodash/get'
-import uniq from 'lodash/uniq'
-import toPath from 'lodash/toPath'
-import head from 'lodash/head'
-import isPlainObject from 'lodash/isPlainObject'
 import negateValue from './negateValue'
 import corePluginList from '../corePluginList'
 import configurePlugins from './configurePlugins'
 import defaultConfig from '../../stubs/defaultConfig.stub'
 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
+}
+
+function mergeWith(target, ...sources) {
+  let customizer = sources.pop()
+
+  for (let source of sources) {
+    for (let k in source) {
+      let merged = customizer(target[k], source[k])
+
+      if (merged === undefined) {
+        if (isObject(target[k]) && isObject(source[k])) {
+          target[k] = mergeWith(target[k], source[k], customizer)
+        } else {
+          target[k] = source[k]
+        }
+      } else {
+        target[k] = merged
+      }
+    }
+  }
+
+  return target
+}
 
 const configUtils = {
   colors,
@@ -49,7 +75,7 @@ function value(valueToResolve, ...args) {
 function collectExtends(items) {
   return items.reduce((merged, { extend }) => {
     return mergeWith(merged, extend, (mergedValue, extendValue) => {
-      if (isUndefined(mergedValue)) {
+      if (mergedValue === undefined) {
         return [extendValue]
       }
 
@@ -74,12 +100,12 @@ function mergeThemes(themes) {
 
 function mergeExtensionCustomizer(merged, value) {
   // When we have an array of objects, we do want to merge it
-  if (Array.isArray(merged) && isPlainObject(head(merged))) {
+  if (Array.isArray(merged) && isObject(merged[0])) {
     return merged.concat(value)
   }
 
   // When the incoming value is an array, and the existing config is an object, prepend the existing object
-  if (Array.isArray(value) && isPlainObject(head(value)) && isPlainObject(merged)) {
+  if (Array.isArray(value) && isObject(value[0]) && isObject(merged)) {
     return [merged, ...value]
   }
 
@@ -95,7 +121,7 @@ function mergeExtensionCustomizer(merged, value) {
 function mergeExtensions({ extend, ...theme }) {
   return mergeWith(theme, extend, (themeValue, extensions) => {
     // The `extend` property is an array, so we need to check if it contains any functions
-    if (!isFunction(themeValue) && !some(extensions, isFunction)) {
+    if (!isFunction(themeValue) && !extensions.some(isFunction)) {
       return mergeWith({}, themeValue, ...extensions, mergeExtensionCustomizer)
     }
 
@@ -143,7 +169,7 @@ function extractPluginConfigs(configs) {
   configs.forEach((config) => {
     allConfigs = [...allConfigs, config]
 
-    const plugins = get(config, 'plugins', [])
+    const plugins = config?.plugins ?? []
 
     if (plugins.length === 0) {
       return
@@ -153,7 +179,7 @@ function extractPluginConfigs(configs) {
       if (plugin.__isOptionsFunction) {
         plugin = plugin()
       }
-      allConfigs = [...allConfigs, ...extractPluginConfigs([get(plugin, 'config', {})])]
+      allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])]
     })
   })
 
@@ -166,9 +192,9 @@ function mergeVariants(variants) {
       if (isFunction(pluginVariants)) {
         resolved[plugin] = pluginVariants({
           variants(path) {
-            return get(resolved, path, [])
+            return dlv(resolved, path, [])
           },
-          before(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
+          before(toInsert, variant, existingPluginVariants = resolved?.[plugin] ?? []) {
             if (variant === undefined) {
               return [...toInsert, ...existingPluginVariants]
             }
@@ -185,7 +211,7 @@ function mergeVariants(variants) {
               ...existingPluginVariants.slice(index),
             ]
           },
-          after(toInsert, variant, existingPluginVariants = get(resolved, plugin, [])) {
+          after(toInsert, variant, existingPluginVariants = resolved?.[plugin] ?? []) {
             if (variant === undefined) {
               return [...existingPluginVariants, ...toInsert]
             }
@@ -202,7 +228,7 @@ function mergeVariants(variants) {
               ...existingPluginVariants.slice(index + 1),
             ]
           },
-          without(toRemove, existingPluginVariants = get(resolved, plugin, [])) {
+          without(toRemove, existingPluginVariants = resolved?.[plugin] ?? []) {
             return existingPluginVariants.filter((v) => !toRemove.includes(v))
           },
         })
@@ -279,14 +305,14 @@ export default function resolveConfig(configs) {
     defaults(
       {
         theme: resolveFunctionKeys(
-          mergeExtensions(mergeThemes(map(allConfigs, (t) => get(t, 'theme', {}))))
+          mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
         ),
         variants: resolveVariants(
-          allConfigs.map((c) => get(c, 'variants', {})),
+          allConfigs.map((c) => c?.variants ?? {}),
           variantOrder
         ),
         corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
-        plugins: resolvePluginLists(configs.map((c) => get(c, 'plugins', []))),
+        plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
       },
       ...allConfigs
     )
diff --git a/src/util/tap.js b/src/util/tap.js
new file mode 100644
index 00000000..0590e4bf
--- /dev/null
+++ b/src/util/tap.js
@@ -0,0 +1,4 @@
+export function tap(value, mutator) {
+  mutator(value)
+  return value
+}
diff --git a/src/util/toColorValue.js b/src/util/toColorValue.js
index a721d2c8..288d9078 100644
--- a/src/util/toColorValue.js
+++ b/src/util/toColorValue.js
@@ -1,5 +1,3 @@
-import _ from 'lodash'
-
 export default function toColorValue(maybeFunction) {
-  return _.isFunction(maybeFunction) ? maybeFunction({}) : maybeFunction
+  return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction
 }
diff --git a/src/util/toPath.js b/src/util/toPath.js
new file mode 100644
index 00000000..acf55d8d
--- /dev/null
+++ b/src/util/toPath.js
@@ -0,0 +1,4 @@
+export function toPath(path) {
+  if (Array.isArray(path)) return path
+  return path.split(/[\.\]\[]+/g)
+}
diff --git a/src/util/withAlphaVariable.js b/src/util/withAlphaVariable.js
index 1c9e8d48..70c552cd 100644
--- a/src/util/withAlphaVariable.js
+++ b/src/util/withAlphaVariable.js
@@ -1,12 +1,11 @@
 import * as culori from 'culori'
-import _ from 'lodash'
 
 function isValidColor(color) {
   return culori.parse(color) !== undefined
 }
 
 export function withAlphaValue(color, alphaValue, defaultValue) {
-  if (_.isFunction(color)) {
+  if (typeof color === 'function') {
     return color({ opacityValue: alphaValue })
   }
 
@@ -40,7 +39,7 @@ export function withAlphaValue(color, alphaValue, defaultValue) {
 }
 
 export default function withAlphaVariable({ color, property, variable }) {
-  if (_.isFunction(color)) {
+  if (typeof color === 'function') {
     return {
       [variable]: '1',
       [property]: color({ opacityVariable: variable, opacityValue: `var(${variable})` }),
diff --git a/stubs/defaultConfig.stub.js b/stubs/defaultConfig.stub.js
index 2230f16e..47051b2a 100644
--- a/stubs/defaultConfig.stub.js
+++ b/stubs/defaultConfig.stub.js
@@ -1,5 +1,3 @@
-const colors = require('../colors')
-
 module.exports = {
   content: [],
   presets: [],
@@ -12,21 +10,34 @@ module.exports = {
       xl: '1280px',
       '2xl': '1536px',
     },
-    colors: {
-      transparent: 'transparent',
-      current: 'currentColor',
-
+    colors: ({ colors }) => ({
+      transparent: colors.transparent,
+      current: colors.current,
       black: colors.black,
       white: colors.white,
-      gray: colors.coolGray,
+      slate: colors.slate,
+      gray: colors.gray,
+      zinc: colors.zinc,
+      neutral: colors.neutral,
+      stone: colors.stone,
       red: colors.red,
-      yellow: colors.amber,
-      green: colors.emerald,
+      orange: colors.orange,
+      amber: colors.amber,
+      yellow: colors.yellow,
+      lime: colors.lime,
+      green: colors.green,
+      emerald: colors.emerald,
+      teal: colors.teal,
+      cyan: colors.cyan,
+      sky: colors.sky,
       blue: colors.blue,
       indigo: colors.indigo,
-      purple: colors.violet,
+      violet: colors.violet,
+      purple: colors.purple,
+      fuchsia: colors.fuchsia,
       pink: colors.pink,
-    },
+      rose: colors.rose,
+    }),
     spacing: {
       px: '1px',
       0: '0px',
@@ -172,6 +183,10 @@ module.exports = {
       none: 'none',
     },
     caretColor: (theme) => theme('colors'),
+    accentColor: (theme) => ({
+      ...theme('colors'),
+      auto: 'auto',
+    }),
     contrast: {
       0: '0',
       50: '.5',


Test patch:
diff --git a/tests/apply.test.js b/tests/apply.test.js
index 0c96c273..182e0214 100644
--- a/tests/apply.test.js
+++ b/tests/apply.test.js
@@ -1,7 +1,7 @@
 import fs from 'fs'
 import path from 'path'
 
-import { run, css } from './util/run'
+import { run, html, css } from './util/run'
 
 test('@apply', () => {
   let config = {
@@ -236,7 +236,7 @@ test('@apply error when using a prefixed .group utility', async () => {
   let config = {
     prefix: 'tw-',
     darkMode: 'class',
-    content: [{ raw: '<div class="foo"></div>' }],
+    content: [{ raw: html`<div class="foo"></div>` }],
   }
 
   let input = css`
@@ -254,3 +254,151 @@ test('@apply error when using a prefixed .group utility', async () => {
     `@apply should not be used with the 'tw-group' utility`
   )
 })
+
+test('@apply classes from outside a @layer', async () => {
+  let config = {
+    content: [{ raw: html`<div class="font-bold foo bar baz"></div>` }],
+  }
+
+  let input = css`
+    @tailwind components;
+    @tailwind utilities;
+
+    .foo {
+      @apply font-bold;
+    }
+
+    .bar {
+      @apply foo text-red-500 hover:text-green-500;
+    }
+
+    .baz {
+      @apply bar underline;
+    }
+
+    .keep-me-even-though-I-am-not-used-in-content {
+      color: green;
+    }
+  `
+
+  await run(input, config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      .font-bold {
+        font-weight: 700;
+      }
+
+      .foo {
+        font-weight: 700;
+      }
+
+      .bar {
+        --tw-text-opacity: 1;
+        color: rgba(239, 68, 68, var(--tw-text-opacity));
+        font-weight: 700;
+      }
+
+      .bar:hover {
+        --tw-text-opacity: 1;
+        color: rgba(34, 197, 94, var(--tw-text-opacity));
+      }
+
+      .baz {
+        text-decoration: underline;
+        --tw-text-opacity: 1;
+        color: rgba(239, 68, 68, var(--tw-text-opacity));
+        font-weight: 700;
+      }
+
+      .baz:hover {
+        --tw-text-opacity: 1;
+        color: rgba(34, 197, 94, var(--tw-text-opacity));
+      }
+
+      .keep-me-even-though-I-am-not-used-in-content {
+        color: green;
+      }
+    `)
+  })
+})
+
+test('@applying classes from outside a @layer respects the source order', async () => {
+  let config = {
+    content: [{ raw: html`<div class="container font-bold foo bar baz"></div>` }],
+  }
+
+  let input = css`
+    .baz {
+      @apply bar underline;
+    }
+
+    @tailwind components;
+
+    .keep-me-even-though-I-am-not-used-in-content {
+      color: green;
+    }
+
+    @tailwind utilities;
+
+    .foo {
+      @apply font-bold;
+    }
+
+    .bar {
+      @apply no-underline;
+    }
+  `
+
+  await run(input, config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      .baz {
+        text-decoration: underline;
+        text-decoration: none;
+      }
+
+      .container {
+        width: 100%;
+      }
+      @media (min-width: 640px) {
+        .container {
+          max-width: 640px;
+        }
+      }
+      @media (min-width: 768px) {
+        .container {
+          max-width: 768px;
+        }
+      }
+      @media (min-width: 1024px) {
+        .container {
+          max-width: 1024px;
+        }
+      }
+      @media (min-width: 1280px) {
+        .container {
+          max-width: 1280px;
+        }
+      }
+      @media (min-width: 1536px) {
+        .container {
+          max-width: 1536px;
+        }
+      }
+
+      .keep-me-even-though-I-am-not-used-in-content {
+        color: green;
+      }
+
+      .font-bold {
+        font-weight: 700;
+      }
+
+      .foo {
+        font-weight: 700;
+      }
+
+      .bar {
+        text-decoration: none;
+      }
+    `)
+  })
+})
diff --git a/tests/basic-usage.test.css b/tests/basic-usage.test.css
index ab005dfe..436b4440 100644
--- a/tests/basic-usage.test.css
+++ b/tests/basic-usage.test.css
@@ -480,7 +480,7 @@
 }
 .bg-green-500 {
   --tw-bg-opacity: 1;
-  background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+  background-color: rgba(34, 197, 94, var(--tw-bg-opacity));
 }
 .bg-opacity-20 {
   --tw-bg-opacity: 0.2;
@@ -493,8 +493,8 @@
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(252, 165, 165, 0));
 }
 .via-purple-200 {
-  --tw-gradient-stops: var(--tw-gradient-from), #ddd6fe,
-    var(--tw-gradient-to, rgba(221, 214, 254, 0));
+  --tw-gradient-stops: var(--tw-gradient-from), #e9d5ff,
+    var(--tw-gradient-to, rgba(233, 213, 255, 0));
 }
 .to-blue-400 {
   --tw-gradient-to: #60a5fa;
@@ -641,7 +641,7 @@
 }
 .placeholder-green-300::placeholder {
   --tw-placeholder-opacity: 1;
-  color: rgba(110, 231, 183, var(--tw-placeholder-opacity));
+  color: rgba(134, 239, 172, var(--tw-placeholder-opacity));
 }
 .placeholder-opacity-60::placeholder {
   --tw-placeholder-opacity: 0.6;
@@ -649,6 +649,9 @@
 .caret-red-600 {
   caret-color: #dc2626;
 }
+.accent-red-600 {
+  accent-color: #dc2626;
+}
 .opacity-90 {
   opacity: 0.9;
 }
diff --git a/tests/basic-usage.test.html b/tests/basic-usage.test.html
index b6e86d26..e21a6c79 100644
--- a/tests/basic-usage.test.html
+++ b/tests/basic-usage.test.html
@@ -101,6 +101,7 @@
     <div class="placeholder-green-300"></div>
     <div class="placeholder-opacity-60"></div>
     <div class="caret-red-600"></div>
+    <div class="accent-red-600"></div>
     <div class="place-items-end"></div>
     <div class="place-self-center"></div>
     <div class="pointer-events-none"></div>
diff --git a/tests/custom-plugins.test.js b/tests/custom-plugins.test.js
new file mode 100644
index 00000000..1256ff8b
--- /dev/null
+++ b/tests/custom-plugins.test.js
@@ -0,0 +1,1894 @@
+import createPlugin from './../plugin'
+import { run, html, css } from './util/run'
+
+test('plugins can create utilities with object syntax', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-object-fill custom-object-contain custom-object-cover"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities({
+          '.custom-object-fill': {
+            'object-fit': 'fill',
+          },
+          '.custom-object-contain': {
+            'object-fit': 'contain',
+          },
+          '.custom-object-cover': {
+            'object-fit': 'cover',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-object-fill {
+        object-fit: fill;
+      }
+      .custom-object-contain {
+        object-fit: contain;
+      }
+      .custom-object-cover {
+        object-fit: cover;
+      }
+    `)
+  })
+})
+
+test('plugins can create utilities with arrays of objects', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-object-fill custom-object-contain custom-object-cover"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities([
+          {
+            '.custom-object-fill': {
+              'object-fit': 'fill',
+            },
+            '.custom-object-contain': {
+              'object-fit': 'contain',
+            },
+            '.custom-object-cover': {
+              'object-fit': 'cover',
+            },
+          },
+        ])
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-object-fill {
+        object-fit: fill;
+      }
+      .custom-object-contain {
+        object-fit: contain;
+      }
+      .custom-object-cover {
+        object-fit: cover;
+      }
+    `)
+  })
+})
+
+test('plugins can create utilities with raw PostCSS nodes', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-object-fill custom-object-contain custom-object-cover"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addUtilities, postcss }) {
+        addUtilities([
+          postcss.rule({ selector: '.custom-object-fill' }).append([
+            postcss.decl({
+              prop: 'object-fit',
+              value: 'fill',
+            }),
+          ]),
+          postcss.rule({ selector: '.custom-object-contain' }).append([
+            postcss.decl({
+              prop: 'object-fit',
+              value: 'contain',
+            }),
+          ]),
+          postcss.rule({ selector: '.custom-object-cover' }).append([
+            postcss.decl({
+              prop: 'object-fit',
+              value: 'cover',
+            }),
+          ]),
+        ])
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-object-fill {
+        object-fit: fill;
+      }
+      .custom-object-contain {
+        object-fit: contain;
+      }
+      .custom-object-cover {
+        object-fit: cover;
+      }
+    `)
+  })
+})
+
+test('plugins can create utilities with mixed object styles and PostCSS nodes', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-object-fill custom-object-contain custom-object-cover"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addUtilities, postcss }) {
+        addUtilities([
+          {
+            '.custom-object-fill': {
+              objectFit: 'fill',
+            },
+          },
+          postcss.rule({ selector: '.custom-object-contain' }).append([
+            postcss.decl({
+              prop: 'object-fit',
+              value: 'contain',
+            }),
+          ]),
+          postcss.rule({ selector: '.custom-object-cover' }).append([
+            postcss.decl({
+              prop: 'object-fit',
+              value: 'cover',
+            }),
+          ]),
+        ])
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-object-fill {
+        object-fit: fill;
+      }
+      .custom-object-contain {
+        object-fit: contain;
+      }
+      .custom-object-cover {
+        object-fit: cover;
+      }
+    `)
+  })
+})
+
+test('plugins can create components with object syntax', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<button class="btn-blue"></button>`,
+      },
+    ],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents({
+          '.btn-blue': {
+            backgroundColor: 'blue',
+            color: 'white',
+            padding: '.5rem 1rem',
+            borderRadius: '.25rem',
+          },
+          '.btn-blue:hover': {
+            backgroundColor: 'darkblue',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn-blue {
+        background-color: blue;
+        color: white;
+        padding: 0.5rem 1rem;
+        border-radius: 0.25rem;
+      }
+      .btn-blue:hover {
+        background-color: darkblue;
+      }
+    `)
+  })
+})
+
+test('plugins can add base styles with object syntax', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<img /><button></button>`,
+      },
+    ],
+    plugins: [
+      function ({ addBase }) {
+        addBase({
+          img: {
+            maxWidth: '100%',
+          },
+          button: {
+            fontFamily: 'inherit',
+          },
+        })
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  return run('@tailwind base', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      img {
+        max-width: 100%;
+      }
+      button {
+        font-family: inherit;
+      }
+    `)
+  })
+})
+
+test('plugins can add base styles with raw PostCSS nodes', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<img /><button></button>`,
+      },
+    ],
+    plugins: [
+      function ({ addBase, postcss }) {
+        addBase([
+          postcss.rule({ selector: 'img' }).append([
+            postcss.decl({
+              prop: 'max-width',
+              value: '100%',
+            }),
+          ]),
+          postcss.rule({ selector: 'button' }).append([
+            postcss.decl({
+              prop: 'font-family',
+              value: 'inherit',
+            }),
+          ]),
+        ])
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  return run('@tailwind base', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      img {
+        max-width: 100%;
+      }
+      button {
+        font-family: inherit;
+      }
+    `)
+  })
+})
+
+test('plugins can create components with raw PostCSS nodes', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<button class="btn-blue"></button>`,
+      },
+    ],
+    plugins: [
+      function ({ addComponents, postcss }) {
+        addComponents([
+          postcss.rule({ selector: '.btn-blue' }).append([
+            postcss.decl({
+              prop: 'background-color',
+              value: 'blue',
+            }),
+            postcss.decl({
+              prop: 'color',
+              value: 'white',
+            }),
+            postcss.decl({
+              prop: 'padding',
+              value: '.5rem 1rem',
+            }),
+            postcss.decl({
+              prop: 'border-radius',
+              value: '.25rem',
+            }),
+          ]),
+          postcss.rule({ selector: '.btn-blue:hover' }).append([
+            postcss.decl({
+              prop: 'background-color',
+              value: 'darkblue',
+            }),
+          ]),
+        ])
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn-blue {
+        background-color: blue;
+        color: white;
+        padding: 0.5rem 1rem;
+        border-radius: 0.25rem;
+      }
+      .btn-blue:hover {
+        background-color: darkblue;
+      }
+    `)
+  })
+})
+
+test('plugins can create components with mixed object styles and raw PostCSS nodes', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<button class="btn-blue"></button>`,
+      },
+    ],
+    plugins: [
+      function ({ addComponents, postcss }) {
+        addComponents([
+          postcss.rule({ selector: '.btn-blue' }).append([
+            postcss.decl({
+              prop: 'background-color',
+              value: 'blue',
+            }),
+            postcss.decl({
+              prop: 'color',
+              value: 'white',
+            }),
+            postcss.decl({
+              prop: 'padding',
+              value: '.5rem 1rem',
+            }),
+            postcss.decl({
+              prop: 'border-radius',
+              value: '.25rem',
+            }),
+          ]),
+          {
+            '.btn-blue:hover': {
+              backgroundColor: 'darkblue',
+            },
+          },
+        ])
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn-blue {
+        background-color: blue;
+        color: white;
+        padding: 0.5rem 1rem;
+        border-radius: 0.25rem;
+      }
+      .btn-blue:hover {
+        background-color: darkblue;
+      }
+    `)
+  })
+})
+
+test('plugins can create components with media queries with object syntax', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-container"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents({
+          '.custom-container': {
+            width: '100%',
+          },
+          '@media (min-width: 100px)': {
+            '.custom-container': {
+              maxWidth: '100px',
+            },
+          },
+          '@media (min-width: 200px)': {
+            '.custom-container': {
+              maxWidth: '200px',
+            },
+          },
+          '@media (min-width: 300px)': {
+            '.custom-container': {
+              maxWidth: '300px',
+            },
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-container {
+        width: 100%;
+      }
+      @media (min-width: 100px) {
+        .custom-container {
+          max-width: 100px;
+        }
+      }
+      @media (min-width: 200px) {
+        .custom-container {
+          max-width: 200px;
+        }
+      }
+      @media (min-width: 300px) {
+        .custom-container {
+          max-width: 300px;
+        }
+      }
+    `)
+  })
+})
+
+test('media queries can be defined multiple times using objects-in-array syntax', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-container"></div>
+          <button class="btn"></button>`,
+      },
+    ],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents([
+          {
+            '.custom-container': {
+              width: '100%',
+            },
+            '@media (min-width: 100px)': {
+              '.custom-container': {
+                maxWidth: '100px',
+              },
+            },
+          },
+          {
+            '.btn': {
+              padding: '1rem .5rem',
+              display: 'block',
+            },
+            '@media (min-width: 100px)': {
+              '.btn': {
+                display: 'inline-block',
+              },
+            },
+          },
+        ])
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-container {
+        width: 100%;
+      }
+      @media (min-width: 100px) {
+        .custom-container {
+          max-width: 100px;
+        }
+      }
+      .btn {
+        padding: 1rem 0.5rem;
+        display: block;
+      }
+      @media (min-width: 100px) {
+        .btn {
+          display: inline-block;
+        }
+      }
+    `)
+  })
+})
+
+test('plugins can create nested rules', () => {
+  let config = {
+    content: [{ raw: html`<button class="btn-blue"></button>` }],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents({
+          '.btn-blue': {
+            backgroundColor: 'blue',
+            color: 'white',
+            padding: '.5rem 1rem',
+            borderRadius: '.25rem',
+            '&:hover': {
+              backgroundColor: 'darkblue',
+            },
+            '@media (min-width: 500px)': {
+              '&:hover': {
+                backgroundColor: 'orange',
+              },
+            },
+            '> a': {
+              color: 'red',
+            },
+            'h1 &': {
+              color: 'purple',
+            },
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn-blue {
+        background-color: blue;
+        color: white;
+        padding: 0.5rem 1rem;
+        border-radius: 0.25rem;
+      }
+      .btn-blue:hover {
+        background-color: darkblue;
+      }
+      @media (min-width: 500px) {
+        .btn-blue:hover {
+          background-color: orange;
+        }
+      }
+      .btn-blue > a {
+        color: red;
+      }
+      h1 .btn-blue {
+        color: purple;
+      }
+    `)
+  })
+})
+
+test('plugins can create rules with escaped selectors', () => {
+  let config = {
+    content: [{ raw: html`<div class="custom-top-1/4"></div>` }],
+    plugins: [
+      function ({ e, addUtilities }) {
+        addUtilities({
+          [`.${e('custom-top-1/4')}`]: {
+            top: '25%',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-top-1\\/4 {
+        top: 25%;
+      }
+    `)
+  })
+})
+
+test('plugins can access the current config', () => {
+  let config = {
+    content: [{ raw: html`<div class="custom-container"></div>` }],
+    plugins: [
+      function ({ addComponents, config }) {
+        let containerClasses = [
+          {
+            '.custom-container': {
+              width: '100%',
+            },
+          },
+        ]
+
+        for (let maxWidth of Object.values(config('theme.customScreens'))) {
+          containerClasses.push({
+            [`@media (min-width: ${maxWidth})`]: {
+              '.custom-container': { maxWidth },
+            },
+          })
+        }
+
+        addComponents(containerClasses)
+      },
+    ],
+    theme: {
+      customScreens: {
+        sm: '576px',
+        md: '768px',
+        lg: '992px',
+        xl: '1200px',
+      },
+    },
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-container {
+        width: 100%;
+      }
+      @media (min-width: 576px) {
+        .custom-container {
+          max-width: 576px;
+        }
+      }
+      @media (min-width: 768px) {
+        .custom-container {
+          max-width: 768px;
+        }
+      }
+      @media (min-width: 992px) {
+        .custom-container {
+          max-width: 992px;
+        }
+      }
+      @media (min-width: 1200px) {
+        .custom-container {
+          max-width: 1200px;
+        }
+      }
+    `)
+  })
+})
+
+test('plugins can check if corePlugins are enabled', () => {
+  let config = {
+    content: [{ raw: html`<div class="test"></div>` }],
+    plugins: [
+      function ({ addUtilities, corePlugins }) {
+        addUtilities({
+          '.test': {
+            'text-color': corePlugins('textColor') ? 'true' : 'false',
+            opacity: corePlugins('opacity') ? 'true' : 'false',
+          },
+        })
+      },
+    ],
+    corePlugins: { textColor: false },
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .test {
+        text-color: false;
+        opacity: true;
+      }
+    `)
+  })
+})
+
+test('plugins can check if corePlugins are enabled when using array white-listing', () => {
+  let config = {
+    content: [{ raw: html`<div class="test"></div>` }],
+    plugins: [
+      function ({ addUtilities, corePlugins }) {
+        addUtilities({
+          '.test': {
+            'text-color': corePlugins('textColor') ? 'true' : 'false',
+            opacity: corePlugins('opacity') ? 'true' : 'false',
+          },
+        })
+      },
+    ],
+    corePlugins: ['textColor'],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .test {
+        text-color: true;
+        opacity: false;
+      }
+    `)
+  })
+})
+
+test('plugins can provide fallbacks to keys missing from the config', () => {
+  let config = {
+    content: [{ raw: html`<button class="btn"></button>` }],
+    plugins: [
+      function ({ addComponents, config }) {
+        addComponents({
+          '.btn': {
+            borderRadius: config('borderRadius.default', '.25rem'),
+          },
+        })
+      },
+    ],
+    theme: {
+      borderRadius: {
+        1: '1px',
+        2: '2px',
+        4: '4px',
+        8: '8px',
+      },
+    },
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn {
+        border-radius: 0.25rem;
+      }
+    `)
+  })
+})
+
+test('plugins can add multiple sets of utilities and components', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<button class="btn"></button>
+          <div class="card custom-skew-12deg custom-border-collapse"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addUtilities, addComponents }) {
+        addComponents({
+          '.card': {
+            padding: '1rem',
+            borderRadius: '.25rem',
+          },
+        })
+
+        addUtilities({
+          '.custom-skew-12deg': {
+            transform: 'skewY(-12deg)',
+          },
+        })
+
+        addComponents({
+          '.btn': {
+            padding: '1rem .5rem',
+            display: 'inline-block',
+          },
+        })
+
+        addUtilities({
+          '.custom-border-collapse': {
+            borderCollapse: 'collapse',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components; @tailwind utilities;', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .card {
+        padding: 1rem;
+        border-radius: 0.25rem;
+      }
+      .btn {
+        padding: 1rem 0.5rem;
+        display: inline-block;
+      }
+      .custom-skew-12deg {
+        transform: skewY(-12deg);
+      }
+      .custom-border-collapse {
+        border-collapse: collapse;
+      }
+    `)
+  })
+})
+
+test('plugins respect prefix and important options by default when adding utilities', () => {
+  let config = {
+    prefix: 'tw-',
+    important: true,
+    content: [{ raw: html`<div class="tw-custom-rotate-90"></div>` }],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities({
+          '.custom-rotate-90': {
+            transform: 'rotate(90deg)',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .tw-custom-rotate-90 {
+        transform: rotate(90deg) !important;
+      }
+    `)
+  })
+})
+
+test('when important is a selector it is used to scope utilities instead of adding !important', () => {
+  let config = {
+    prefix: 'tw-',
+    important: '#app',
+    content: [{ raw: html`<div class="tw-custom-rotate-90"></div>` }],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities({
+          '.custom-rotate-90': {
+            transform: 'rotate(90deg)',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      #app .tw-custom-rotate-90 {
+        transform: rotate(90deg);
+      }
+    `)
+  })
+})
+
+test('when important is a selector it scopes all selectors in a rule, even though defining utilities like this is stupid', () => {
+  let config = {
+    important: '#app',
+    content: [{ raw: html`<div class="custom-rotate-90 custom-rotate-1/4"></div>` }],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities({
+          '.custom-rotate-90, .custom-rotate-1\\/4': {
+            transform: 'rotate(90deg)',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      #app .custom-rotate-90,
+      #app .custom-rotate-1\\/4 {
+        transform: rotate(90deg);
+        transform: rotate(90deg);
+      }
+    `)
+  })
+})
+
+test('important utilities are not made double important when important option is used', () => {
+  let config = {
+    important: true,
+    content: [{ raw: html`<div class="custom-rotate-90"></div>` }],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities({
+          '.custom-rotate-90': {
+            transform: 'rotate(90deg) !important',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-rotate-90 {
+        transform: rotate(90deg) !important;
+      }
+    `)
+  })
+})
+
+test("component declarations respect the 'prefix' option by default", () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`<button class="tw-btn-blue"></button>` }],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents({
+          '.btn-blue': {
+            backgroundColor: 'blue',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .tw-btn-blue {
+        background-color: blue;
+      }
+    `)
+  })
+})
+
+test('all selectors in a rule are prefixed', () => {
+  let config = {
+    prefix: 'tw-',
+    content: [
+      {
+        raw: html`<button class="tw-btn-blue tw-btn-red"></button>
+          <div class="tw-custom-rotate-90 tw-custom-rotate-1/4"></div>`,
+      },
+    ],
+    plugins: [
+      function ({ addUtilities, addComponents }) {
+        addUtilities({
+          '.custom-rotate-90, .custom-rotate-1\\/4': {
+            transform: 'rotate(90deg)',
+          },
+        })
+        addComponents({
+          '.btn-blue, .btn-red': {
+            padding: '10px',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .tw-btn-blue,
+      .tw-btn-red {
+        padding: 10px;
+        padding: 10px;
+      }
+    `)
+  })
+})
+
+test("component declarations can optionally ignore 'prefix' option", () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`<button class="btn-blue"></button>` }],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents(
+          {
+            '.btn-blue': {
+              backgroundColor: 'blue',
+            },
+          },
+          { respectPrefix: false }
+        )
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn-blue {
+        background-color: blue;
+      }
+    `)
+  })
+})
+
+test("component declarations are not affected by the 'important' option", () => {
+  let config = {
+    important: true,
+    content: [{ raw: html`<button class="btn-blue"></button>` }],
+    plugins: [
+      function ({ addComponents }) {
+        addComponents({
+          '.btn-blue': {
+            backgroundColor: 'blue',
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .btn-blue {
+        background-color: blue;
+      }
+    `)
+  })
+})
+
+test("plugins can apply the user's chosen prefix to components manually", () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`<button class="tw-btn-blue"></button>` }],
+    plugins: [
+      function ({ addComponents, prefix }) {
+        addComponents(
+          {
+            [prefix('.btn-blue')]: {
+              backgroundColor: 'blue',
+            },
+          },
+          { respectPrefix: false }
+        )
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .tw-btn-blue {
+        background-color: blue;
+      }
+    `)
+  })
+})
+
+test('prefix can optionally be ignored for utilities', () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`<div class="custom-rotate-90"></div>` }],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities(
+          {
+            '.custom-rotate-90': {
+              transform: 'rotate(90deg)',
+            },
+          },
+          { respectPrefix: false }
+        )
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-rotate-90 {
+        transform: rotate(90deg);
+      }
+    `)
+  })
+})
+
+test('important can optionally be ignored for utilities', () => {
+  let config = {
+    important: true,
+    content: [{ raw: html`<div class="custom-rotate-90"></div>` }],
+    plugins: [
+      function ({ addUtilities }) {
+        addUtilities(
+          {
+            '.custom-rotate-90': {
+              transform: 'rotate(90deg)',
+            },
+          },
+          { respectImportant: false }
+        )
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-rotate-90 {
+        transform: rotate(90deg);
+      }
+    `)
+  })
+})
+
+test('prefix will prefix all classes in a selector', () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`<div class="tw-btn-blue tw-w-1/4"></div>` }],
+    plugins: [
+      function ({ addComponents, prefix }) {
+        addComponents(
+          {
+            [prefix('.btn-blue .w-1\\/4 > h1.text-xl + a .bar')]: {
+              backgroundColor: 'blue',
+            },
+          },
+          { respectPrefix: false }
+        )
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .tw-btn-blue .tw-w-1\\/4 > h1.tw-text-xl + a .tw-bar {
+        background-color: blue;
+      }
+    `)
+  })
+})
+
+test('plugins can be provided as an object with a handler function', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div class="custom-object-fill custom-object-contain custom-object-cover"></div>`,
+      },
+    ],
+    plugins: [
+      {
+        handler({ addUtilities }) {
+          addUtilities({
+            '.custom-object-fill': {
+              'object-fit': 'fill',
+            },
+            '.custom-object-contain': {
+              'object-fit': 'contain',
+            },
+            '.custom-object-cover': {
+              'object-fit': 'cover',
+            },
+          })
+        },
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .custom-object-fill {
+        object-fit: fill;
+      }
+      .custom-object-contain {
+        object-fit: contain;
+      }
+      .custom-object-cover {
+        object-fit: cover;
+      }
+    `)
+  })
+})
+
+test('plugins can provide a config but no handler', () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div
+          class="tw-custom-object-fill tw-custom-object-contain tw-custom-object-cover"
+        ></div>`,
+      },
+    ],
+    plugins: [
+      {
+        config: {
+          prefix: 'tw-',
+        },
+      },
+      {
+        handler({ addUtilities }) {
+          addUtilities({
+            '.custom-object-fill': {
+              'object-fit': 'fill',
+            },
+            '.custom-object-contain': {
+              'object-fit': 'contain',
+            },
+            '.custom-object-cover': {
+              'object-fit': 'cover',
+            },
+          })
+        },
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .tw-custom-object-fill {
+        object-fit: fill;
+      }
+      .tw-custom-object-contain {
+        object-fit: contain;
+      }
+      .tw-custom-object-cover {
+        object-fit: cover;
+      }
+    `)
+  })
+})
+
+test('plugins can be created using the `createPlugin` function', () => {
+  let config = {
+    content: [{ raw: html`<div class="test-sm test-md test-lg hover:test-sm sm:test-sm"></div>` }],
+    corePlugins: [],
+    theme: {
+      screens: {
+        sm: '400px',
+      },
+    },
+    plugins: [
+      createPlugin(
+        function ({ addUtilities, theme, variants }) {
+          addUtilities(
+            Object.fromEntries(
+              Object.entries(theme('testPlugin')).map(([k, v]) => [
+                `.test-${k}`,
+                { testProperty: v },
+              ])
+            ),
+            variants('testPlugin')
+          )
+        },
+        {
+          theme: {
+            testPlugin: {
+              sm: '1rem',
+              md: '2rem',
+              lg: '3rem',
+            },
+          },
+        }
+      ),
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .test-sm {
+        test-property: 1rem;
+      }
+      .test-md {
+        test-property: 2rem;
+      }
+      .test-lg {
+        test-property: 3rem;
+      }
+      .hover\\:test-sm:hover {
+        test-property: 1rem;
+      }
+      @media (min-width: 400px) {
+        .sm\\:test-sm {
+          test-property: 1rem;
+        }
+      }
+    `)
+  })
+})
+
+test('plugins with extra options can be created using the `createPlugin.withOptions` function', () => {
+  let plugin = createPlugin.withOptions(
+    function ({ className }) {
+      return function ({ addUtilities, theme }) {
+        addUtilities(
+          Object.fromEntries(
+            Object.entries(theme('testPlugin')).map(([k, v]) => [
+              `.${className}-${k}`,
+              { testProperty: v },
+            ])
+          )
+        )
+      }
+    },
+    function () {
+      return {
+        theme: {
+          testPlugin: {
+            sm: '1rem',
+            md: '2rem',
+            lg: '3rem',
+          },
+        },
+      }
+    }
+  )
+
+  let config = {
+    content: [
+      { raw: html`<div class="banana-sm banana-md banana-lg hover:banana-sm sm:banana-sm"></div>` },
+    ],
+    corePlugins: [],
+    theme: {
+      screens: {
+        sm: '400px',
+      },
+    },
+    plugins: [plugin({ className: 'banana' })],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .banana-sm {
+        test-property: 1rem;
+      }
+      .banana-md {
+        test-property: 2rem;
+      }
+      .banana-lg {
+        test-property: 3rem;
+      }
+      .hover\\:banana-sm:hover {
+        test-property: 1rem;
+      }
+      @media (min-width: 400px) {
+        .sm\\:banana-sm {
+          test-property: 1rem;
+        }
+      }
+    `)
+  })
+})
+
+test('plugins should cache correctly', () => {
+  let plugin = createPlugin.withOptions(({ className = 'banana' } = {}) => ({ addComponents }) => {
+    addComponents({ [`.${className}`]: { position: 'absolute' } })
+  })
+
+  let config = {
+    content: [{ raw: html`<div class="banana sm:banana apple sm:apple"></div>` }],
+    corePlugins: [],
+    theme: {
+      screens: {
+        sm: '400px',
+      },
+    },
+  }
+
+  function internalRun(options = {}) {
+    return run('@tailwind components', {
+      ...config,
+      plugins: [plugin(options)],
+    })
+  }
+
+  return Promise.all([internalRun(), internalRun({ className: 'apple' })]).then(
+    ([result1, result2]) => {
+      let expected1 = css`
+        .banana {
+          position: absolute;
+        }
+
+        @media (min-width: 400px) {
+          .sm\\:banana {
+            position: absolute;
+          }
+        }
+      `
+
+      let expected2 = css`
+        .apple {
+          position: absolute;
+        }
+
+        @media (min-width: 400px) {
+          .sm\\:apple {
+            position: absolute;
+          }
+        }
+      `
+
+      expect(result1.css).toMatchCss(expected1)
+      expect(result2.css).toMatchCss(expected2)
+    }
+  )
+})
+
+test('plugins created using `createPlugin.withOptions` do not need to be invoked if the user wants to use the default options', () => {
+  let plugin = createPlugin.withOptions(
+    function ({ className } = { className: 'banana' }) {
+      return function ({ addUtilities, theme }) {
+        addUtilities(
+          Object.fromEntries(
+            Object.entries(theme('testPlugin')).map(([k, v]) => [
+              `.${className}-${k}`,
+              { testProperty: v },
+            ])
+          )
+        )
+      }
+    },
+    function () {
+      return {
+        theme: {
+          testPlugin: {
+            sm: '1rem',
+            md: '2rem',
+            lg: '3rem',
+          },
+        },
+        variants: {
+          testPlugin: ['responsive', 'hover'],
+        },
+      }
+    }
+  )
+
+  let config = {
+    content: [
+      { raw: html`<div class="banana-sm banana-md banana-lg hover:banana-sm sm:banana-sm"></div>` },
+    ],
+    corePlugins: [],
+    theme: {
+      screens: {
+        sm: '400px',
+      },
+    },
+    plugins: [plugin],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .banana-sm {
+        test-property: 1rem;
+      }
+      .banana-md {
+        test-property: 2rem;
+      }
+      .banana-lg {
+        test-property: 3rem;
+      }
+      .hover\\:banana-sm:hover {
+        test-property: 1rem;
+      }
+      @media (min-width: 400px) {
+        .sm\\:banana-sm {
+          test-property: 1rem;
+        }
+      }
+    `)
+  })
+})
+
+test('the configFunction parameter is optional when using the `createPlugin.withOptions` function', () => {
+  let plugin = createPlugin.withOptions(function ({ className }) {
+    return function ({ addUtilities, theme }) {
+      addUtilities(
+        Object.fromEntries(
+          Object.entries(theme('testPlugin')).map(([k, v]) => [
+            `.${className}-${k}`,
+            { testProperty: v },
+          ])
+        )
+      )
+    }
+  })
+
+  let config = {
+    content: [
+      { raw: html`<div class="banana-sm banana-md banana-lg hover:banana-sm sm:banana-sm"></div>` },
+    ],
+    corePlugins: [],
+    theme: {
+      screens: {
+        sm: '400px',
+      },
+      testPlugin: {
+        sm: '1px',
+        md: '2px',
+        lg: '3px',
+      },
+    },
+    plugins: [plugin({ className: 'banana' })],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .banana-sm {
+        test-property: 1px;
+      }
+      .banana-md {
+        test-property: 2px;
+      }
+      .banana-lg {
+        test-property: 3px;
+      }
+      .hover\\:banana-sm:hover {
+        test-property: 1px;
+      }
+      @media (min-width: 400px) {
+        .sm\\:banana-sm {
+          test-property: 1px;
+        }
+      }
+    `)
+  })
+})
+
+test('keyframes are not escaped', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo-[abc]"></div>` }],
+    plugins: [
+      function ({ matchUtilities }) {
+        matchUtilities({
+          foo: (value, { includeRules }) => {
+            includeRules(
+              [
+                {
+                  [`@keyframes ${value}`]: {
+                    '25.001%': {
+                      color: 'black',
+                    },
+                  },
+                },
+              ],
+              { respectImportant: false }
+            )
+
+            return {
+              animation: `${value} 1s infinite`,
+            }
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      @keyframes abc {
+        25.001% {
+          color: black;
+        }
+      }
+      .foo-\\[abc\\] {
+        animation: abc 1s infinite;
+      }
+    `)
+  })
+})
+
+test('font sizes are retrieved without default line-heights or letter-spacing using theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      fontSize: {
+        sm: ['14px', '20px'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            fontSize: theme('fontSize.sm'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        font-size: 14px;
+      }
+    `)
+  })
+})
+
+test('outlines are retrieved without outline-offset using theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      outline: {
+        black: ['2px dotted black', '4px'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            outline: theme('outline.black'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        outline: 2px dotted black;
+      }
+    `)
+  })
+})
+
+test('box-shadow values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      boxShadow: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            boxShadow: theme('boxShadow.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        box-shadow: width, height;
+      }
+    `)
+  })
+})
+
+test('transition-property values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      transitionProperty: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            transitionProperty: theme('transitionProperty.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        transition-property: width, height;
+      }
+    `)
+  })
+})
+
+test('transition-duration values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      transitionDuration: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            transitionDuration: theme('transitionDuration.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        transition-duration: width, height;
+      }
+    `)
+  })
+})
+
+test('transition-delay values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      transitionDuration: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            transitionDuration: theme('transitionDuration.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        transition-duration: width, height;
+      }
+    `)
+  })
+})
+
+test('transition-timing-function values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      transitionTimingFunction: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            transitionTimingFunction: theme('transitionTimingFunction.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        transition-timing-function: width, height;
+      }
+    `)
+  })
+})
+
+test('background-image values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      backgroundImage: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            backgroundImage: theme('backgroundImage.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        background-image: width, height;
+      }
+    `)
+  })
+})
+
+test('background-size values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      backgroundSize: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            backgroundSize: theme('backgroundSize.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        background-size: width, height;
+      }
+    `)
+  })
+})
+
+test('background-color values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      backgroundColor: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            backgroundColor: theme('backgroundColor.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        background-color: width, height;
+      }
+    `)
+  })
+})
+
+test('cursor values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      cursor: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            cursor: theme('cursor.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        cursor: width, height;
+      }
+    `)
+  })
+})
+
+test('animation values are joined when retrieved using the theme function', () => {
+  let config = {
+    content: [{ raw: html`<div class="foo"></div>` }],
+    corePlugins: [],
+    theme: {
+      animation: {
+        lol: ['width', 'height'],
+      },
+    },
+    plugins: [
+      function ({ addComponents, theme }) {
+        addComponents({
+          '.foo': {
+            animation: theme('animation.lol'),
+          },
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .foo {
+        animation: width, height;
+      }
+    `)
+  })
+})
diff --git a/tests/kitchen-sink.test.css b/tests/kitchen-sink.test.css
index ffd0e7dc..cfda4f5c 100644
--- a/tests/kitchen-sink.test.css
+++ b/tests/kitchen-sink.test.css
@@ -29,7 +29,7 @@
 @media (min-width: 640px) {
   .apply-test {
     --tw-bg-opacity: 1;
-    background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+    background-color: rgba(34, 197, 94, var(--tw-bg-opacity));
   }
   .apply-test:focus:nth-child(even) {
     --tw-bg-opacity: 1;
@@ -83,7 +83,7 @@
 @media (min-width: 640px) {
   .apply-with-existing:hover {
     --tw-bg-opacity: 1;
-    background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+    background-color: rgba(34, 197, 94, var(--tw-bg-opacity));
   }
 }
 .multiple,
@@ -117,7 +117,7 @@
 }
 .dark .group:hover .apply-dark-group-example-a {
   --tw-bg-opacity: 1;
-  background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+  background-color: rgba(34, 197, 94, var(--tw-bg-opacity));
 }
 @media (min-width: 640px) {
   @media (prefers-reduced-motion: no-preference) {
@@ -292,7 +292,7 @@ div {
 }
 .bg-green-500 {
   --tw-bg-opacity: 1;
-  background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+  background-color: rgba(34, 197, 94, var(--tw-bg-opacity));
 }
 .bg-opacity-50 {
   --tw-bg-opacity: 0.5;
diff --git a/tests/raw-content.test.css b/tests/raw-content.test.css
index 77169466..c4d4fa05 100644
--- a/tests/raw-content.test.css
+++ b/tests/raw-content.test.css
@@ -399,7 +399,7 @@
 }
 .bg-green-500 {
   --tw-bg-opacity: 1;
-  background-color: rgba(16, 185, 129, var(--tw-bg-opacity));
+  background-color: rgba(34, 197, 94, var(--tw-bg-opacity));
 }
 .bg-opacity-20 {
   --tw-bg-opacity: 0.2;
@@ -412,8 +412,8 @@
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgba(252, 165, 165, 0));
 }
 .via-purple-200 {
-  --tw-gradient-stops: var(--tw-gradient-from), #ddd6fe,
-    var(--tw-gradient-to, rgba(221, 214, 254, 0));
+  --tw-gradient-stops: var(--tw-gradient-from), #e9d5ff,
+    var(--tw-gradient-to, rgba(233, 213, 255, 0));
 }
 .to-blue-400 {
   --tw-gradient-to: #60a5fa;
@@ -560,7 +560,7 @@
 }
 .placeholder-green-300::placeholder {
   --tw-placeholder-opacity: 1;
-  color: rgba(110, 231, 183, var(--tw-placeholder-opacity));
+  color: rgba(134, 239, 172, var(--tw-placeholder-opacity));
 }
 .placeholder-opacity-60::placeholder {
   --tw-placeholder-opacity: 0.6;
diff --git a/tests/resolveConfig.test.js b/tests/resolveConfig.test.js
index bcba03bd..b5b6147e 100644
--- a/tests/resolveConfig.test.js
+++ b/tests/resolveConfig.test.js
@@ -1320,13 +1320,16 @@ test('custom properties are multiplied by -1 for negative values', () => {
   const userConfig = {
     theme: {
       spacing: {
+        0: 0,
         1: '1px',
         2: '2px',
         3: '3px',
         4: '4px',
+        auto: 'auto',
         foo: 'var(--foo)',
         bar: 'var(--bar, 500px)',
         baz: 'calc(50% - 10px)',
+        qux: '10poops',
       },
       margin: (theme, { negative }) => ({
         ...theme('spacing'),
@@ -1345,38 +1348,39 @@ test('custom properties are multiplied by -1 for negative values', () => {
 
   const result = resolveConfig([userConfig, defaultConfig])
 
-  expect(result).toMatchObject({
-    prefix: '-',
-    important: false,
-    separator: ':',
-    theme: {
-      spacing: {
-        1: '1px',
-        2: '2px',
-        3: '3px',
-        4: '4px',
-        foo: 'var(--foo)',
-        bar: 'var(--bar, 500px)',
-        baz: 'calc(50% - 10px)',
-      },
-      margin: {
-        1: '1px',
-        2: '2px',
-        3: '3px',
-        4: '4px',
-        foo: 'var(--foo)',
-        bar: 'var(--bar, 500px)',
-        baz: 'calc(50% - 10px)',
-        '-1': '-1px',
-        '-2': '-2px',
-        '-3': '-3px',
-        '-4': '-4px',
-        '-foo': 'calc(var(--foo) * -1)',
-        '-bar': 'calc(var(--bar, 500px) * -1)',
-        '-baz': 'calc(-50% - -10px)',
-      },
-    },
-    variants: {},
+  expect(result.theme.spacing).toEqual({
+    0: 0,
+    1: '1px',
+    2: '2px',
+    3: '3px',
+    4: '4px',
+    auto: 'auto',
+    foo: 'var(--foo)',
+    bar: 'var(--bar, 500px)',
+    baz: 'calc(50% - 10px)',
+    qux: '10poops',
+  })
+  expect(result.theme.margin).toEqual({
+    0: 0,
+    1: '1px',
+    2: '2px',
+    3: '3px',
+    4: '4px',
+    auto: 'auto',
+    foo: 'var(--foo)',
+    bar: 'var(--bar, 500px)',
+    baz: 'calc(50% - 10px)',
+    qux: '10poops',
+    '-0': '-0',
+    '-1': '-1px',
+    '-2': '-2px',
+    '-3': '-3px',
+    '-4': '-4px',
+    '-auto': 'auto',
+    '-foo': 'calc(var(--foo) * -1)',
+    '-bar': 'calc(var(--bar, 500px) * -1)',
+    '-baz': 'calc(calc(50% - 10px) * -1)',
+    '-qux': '-10poops',
   })
 })
 
diff --git a/tests/to-path.test.js b/tests/to-path.test.js
new file mode 100644
index 00000000..dbd39c54
--- /dev/null
+++ b/tests/to-path.test.js
@@ -0,0 +1,17 @@
+import { toPath } from '../src/util/toPath'
+
+it('should keep an array as an array', () => {
+  let input = ['a', 'b', '0', 'c']
+
+  expect(toPath(input)).toBe(input)
+})
+
+it.each`
+  input         | output
+  ${'a.b.c'}    | ${['a', 'b', 'c']}
+  ${'a[0].b.c'} | ${['a', '0', 'b', 'c']}
+  ${'.a'}       | ${['', 'a']}
+  ${'[].a'}     | ${['', 'a']}
+`('should convert "$input" to "$output"', ({ input, output }) => {
+  expect(toPath(input)).toEqual(output)
+})
diff --git a/tests/util/invokePlugin.js b/tests/util/invokePlugin.js
index c58d1f07..226d6470 100644
--- a/tests/util/invokePlugin.js
+++ b/tests/util/invokePlugin.js
@@ -1,16 +1,16 @@
-import _ from 'lodash'
+import dlv from 'dlv'
 import escapeClassName from '../src/util/escapeClassName'
 
 export default function (plugin, config) {
   const addedUtilities = []
 
-  const getConfigValue = (path, defaultValue) => _.get(config, path, defaultValue)
+  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 (_.isArray(config.variants)) {
+      if (Array.isArray(config.variants)) {
         return config.variants
       }
 
@@ -36,7 +36,7 @@ export default function (plugin, config) {
 
   return {
     utilities: addedUtilities.map(([utilities, variants]) => [
-      _.merge({}, ..._.castArray(utilities)),
+      Object.assign({}, ...(Array.isArray(utilities) ? utilities : [utilities])),
       variants,
     ]),
   }
diff --git a/tests/variants.test.css b/tests/variants.test.css
index 666a15c9..56277cc5 100644
--- a/tests/variants.test.css
+++ b/tests/variants.test.css
@@ -15,7 +15,7 @@
 }
 .first-line\:bg-yellow-300::first-line {
   --tw-bg-opacity: 1;
-  background-color: rgba(252, 211, 77, var(--tw-bg-opacity));
+  background-color: rgba(253, 224, 71, var(--tw-bg-opacity));
 }
 .first-line\:underline::first-line {
   text-decoration: underline;