Repository: tailwindlabs/tailwindcss
Pull request #5384 (2021-09-03 15:55:53)

Enable extended color palette
This PR updates the default config to include all of the colors available in the extended color palette.

**This is a breaking change** because several of the colors in the extended palette have names that conflict with the colors in the default palette, but we have included several measures to minimize the impact.

## Changes

### Default colors are no longer aliased

In v2.0, several of the default colors were actually aliases for the extended colors:

| v2 Default | v2 Extended |
| --- | --- |
| `gray` | `coolGray` |
| `green` | `emerald` |
| `yellow` | `amber` |
| `purple` | `violet` |

Although we still think this is a powerful way to think about curating your own color palette (it's nice to type `bg-gray-500` instead of `bg-cool-gray-500` everywhere in your app if you are only using one gray), it caused a lot of confusion and was hard to explain in the documentation.

For v3.0, all of the extended colors will be enabled by default and nothing will be aliased. As a result, the colors that _were_ aliased in v2 will be renamed to their _extended_ palette name, with the exception of `gray`, where we are renaming the extended color rather than the default color.

| v2 Default | v2 Extended | v3 Unified |
| --- | --- | --- |
| `gray` | `coolGray` | `gray` |
| `green` | `emerald` | `emerald` |
| `yellow` | `amber` | `amber` |
| `purple` | `violet` | `violet` |

This means that after upgrading to v3, if you were using utilities like `bg-green-500`, `text-yellow-700`, or `border-purple-400`, the rendered output will be different, as `green` by default now refers to the old _extended_ `green`, whereas in v2, `green` actually meant `emerald` (see what I mean about this being confusing?)

The default `gray` will still look the same after upgrading.

If you want to upgrade without changing the visual appearance of your project (which is what I would recommend), you would need to re-alias these colors in your config:

```js
// tailwind.config.js
let colors = require('tailwindcss/colors')

module.exports = {
  theme: {
    extend: {
      colors: {
        green: colors.emerald,
        purple: colors.violet,
        yellow: colors.amber,
      },
    },
  },
}
```

Of course if you are already using a custom color palette, this won't impact you at all.

### Grays have been renamed

As part of enabling all of the extended colors by default, we've given the different gray shades shorter single-word names to make them more practical to use and make it less awkward for them to co-exist at the same time.

| v2 Default | v2 Extended | v3 Unified |
| --- | --- | --- |
| N/A | `blueGray` | `slate` |
| `gray` | `coolGray` | `gray` |
| N/A | `gray` | `zinc` |
| N/A | `trueGray` | `neutral` |
| N/A | `warmGray` | `stone` |

> Yes I know, `gray` should probably be the name of the neutral gray. The problem is that it would be a very painful breaking change, and breaking changes are very bad when they aren't absolutely necessary. Neutral grays are ugly anyways 🙃

Since only `gray` was enabled in the default v2 palette, this is not a breaking change for anyone who had not aliased any of the extended grays as part of a custom palette.

Furthermore, we've kept all of the old names that we could to ease the upgrade process, and just emit a warning if you import one of the old colors.

For example, this will still totally work, you'll just see a console warning letting you know that you should change `trueGray` to `neutral` in your config file:

```js
// tailwind.config.js
let colors = require('tailwindcss/colors')

module.exports = {
  theme: {
    extend: {
      colors: {
        gray: colors.trueGray,
        // ...
      },
    },
  },
}
```

**The only place this is not true is when importing the v2 extended `gray` color.** Because v3 also has a `gray` (which used to be `coolGray`), it's impossible for us to emit a warning since we can't know if you are using the new gray intentionally, or if you are upgrading and expecting it to be the old gray.

If you were using the old `gray` color in your custom palette, you will need to change it to `zinc` to upgrade with no visual changes:

```diff
  // tailwind.config.js
  let colors = require('tailwindcss/colors')
  
  module.exports = {
    theme: {
      extend: {
        colors: {
-         gray: colors.gray,
+         gray: colors.zinc,
          // ...
        },
      },
    },
  }
```

### Added `transparent` and `current` to `tailwindcss/colors`

Previously, the `transparent` and `current` (`currentColor`) colors were only present in the default config, and not included in `tailwindcss/colors` which meant you had to reference them like this when you wanted to include them in a custom palette:

```js
// tailwind.config.js
let colors = require('tailwindcss/colors')

module.exports = {
  theme: {
    extend: {
      colors: {
        transparent: 'transparent',
        current: 'currentColor',
        gray: colors.trueGray,
        red: colors.rose,
        // ...
      },
    },
  },
}
```

Now, you can reference both of those options from the `colors` object, which feels a little bit more consistent and will help people avoid wasting 30 seconds expecting it to work but then having to change it:

```js
// tailwind.config.js
let colors = require('tailwindcss/colors')

module.exports = {
  theme: {
    extend: {
      colors: {
        transparent: colors.transparent,
        current: colors.current,
        gray: colors.trueGray,
        red: colors.rose,
        // ...
      },
    },
  },
}
```

## Complete color reference

Here is a comprehensive list of how the default (not extended) v2 color palette compares to this new unified v3-destined color palette:

| v2 Default | v2 Extended | v3 Unified |  |
| --- | --- | --- | --- | 
| `transparent` | N/A | `transparent` |  |
| `current` | N/A | `current` |  |
| `black` | `black` | `black` |  |
| `white` | `white` | `white` |  |
| N/A | `blueGray` | `slate` | ⚠️ |
| `gray` | `coolGray` | `gray` | ⚠️ |
| N/A | `gray` | `zinc` | 🚨 |
| N/A | `trueGray` | `neutral` | ⚠️ |
| N/A | `warmGray` | `stone` | ⚠️ |
| `red` | `red` | `red` | |
| N/A | `orange` | `orange` | |
| `yellow` | `amber` | `amber` | 🚨 |
| N/A | `yellow` | `yellow` | |
| N/A | `lime` | `lime` | |
| `green` | `emerald` | `emerald` | 🚨 |
| N/A | `green` | `green` | |
| N/A | `teal` | `teal` | |
| N/A | `cyan` | `cyan` | |
| N/A | `sky` | `sky` | |
| `blue` | `blue` | `blue` | |
| `indigo` | `indigo` | `indigo` | |
| `purple` | `violet` | `violet` | 🚨 |
| N/A | `purple` | `purple` | |
| N/A | `fuchsia` | `fuchsia` | |
| `pink` | `pink` | `pink` | |
| N/A | `rose` | `rose` | |


Patch:
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..9bd32c17 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,6 @@
   "requires": true,
   "packages": {
     "": {
-      "name": "tailwindcss",
       "version": "2.2.9",
       "license": "MIT",
       "dependencies": {
@@ -20,7 +19,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 +27,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"
       },
@@ -3669,11 +3666,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",
@@ -7031,7 +7023,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 +8725,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 +13057,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",
@@ -15617,7 +15591,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 +16816,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..c5f127be 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/*",
@@ -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/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..c6a9732a 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)
@@ -499,7 +462,7 @@ function registerPlugins(plugins, context) {
         pluginItem(pluginApi)
       }
     } else {
-      plugin(pluginApi)
+      plugin?.(pluginApi)
     }
   }
 
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/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/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..e4eb22b7 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',


Test patch:
diff --git a/tests/basic-usage.test.css b/tests/basic-usage.test.css
index ab005dfe..ad5ec5eb 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;
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;