diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -252,222 +252,228 @@ function processApply(root, context, localCache) {
   })
 
   // Start the @apply process if we have rules with @apply in them
-  if (applies.length > 0) {
-    // Fill up some caches!
-    let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
-
-    /**
-     * When we have an apply like this:
-     *
-     * .abc {
-     *    @apply hover:font-bold;
-     * }
-     *
-     * What we essentially will do is resolve to this:
-     *
-     * .abc {
-     *    @apply .hover\:font-bold:hover {
-     *      font-weight: 500;
-     *    }
-     * }
-     *
-     * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
-     * What happens in this function is that we prepend a `.` and escape the candidate.
-     * This will result in `.hover\:font-bold`
-     * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
-     */
-    // TODO: Should we use postcss-selector-parser for this instead?
-    function replaceSelector(selector, utilitySelectors, candidate) {
-      let needle = `.${escapeClassName(candidate)}`
-      let utilitySelectorsList = utilitySelectors.split(/\s*\,(?![^(]*\))\s*/g)
-
-      return selector
-        .split(/\s*\,(?![^(]*\))\s*/g)
-        .map((s) => {
-          let replaced = []
-
-          for (let utilitySelector of utilitySelectorsList) {
-            let replacedSelector = utilitySelector.replace(needle, s)
-            if (replacedSelector === utilitySelector) {
-              continue
-            }
-            replaced.push(replacedSelector)
-          }
-          return replaced.join(', ')
-        })
-        .join(', ')
-    }
+  if (applies.length === 0) {
+    return
+  }
 
-    let perParentApplies = new Map()
+  // Fill up some caches!
+  let applyClassCache = combineCaches([localCache, buildApplyCache(applyCandidates, context)])
+
+  /**
+   * When we have an apply like this:
+   *
+   * .abc {
+   *    @apply hover:font-bold;
+   * }
+   *
+   * What we essentially will do is resolve to this:
+   *
+   * .abc {
+   *    @apply .hover\:font-bold:hover {
+   *      font-weight: 500;
+   *    }
+   * }
+   *
+   * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
+   * What happens in this function is that we prepend a `.` and escape the candidate.
+   * This will result in `.hover\:font-bold`
+   * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
+   */
+  // TODO: Should we use postcss-selector-parser for this instead?
+  function replaceSelector(selector, utilitySelectors, candidate) {
+    let needle = `.${escapeClassName(candidate)}`
+    let needles = [...new Set([needle, needle.replace(/\\2c /g, '\\,')])]
+    let utilitySelectorsList = utilitySelectors.split(/\s*(?<!\\)\,(?![^(]*\))\s*/g)
+
+    return selector
+      .split(/\s*(?<!\\)\,(?![^(]*\))\s*/g)
+      .map((s) => {
+        let replaced = []
+
+        for (let utilitySelector of utilitySelectorsList) {
+          let replacedSelector = utilitySelector
+          for (const needle of needles) {
+            replacedSelector = replacedSelector.replace(needle, s)
+          }
+          if (replacedSelector === utilitySelector) {
+            continue
+          }
+          replaced.push(replacedSelector)
+        }
+        return replaced.join(', ')
+      })
+      .join(', ')
+  }
 
-    // Collect all apply candidates and their rules
-    for (let apply of applies) {
-      let candidates = perParentApplies.get(apply.parent) || []
+  let perParentApplies = new Map()
 
-      perParentApplies.set(apply.parent, [candidates, apply.source])
+  // Collect all apply candidates and their rules
+  for (let apply of applies) {
+    let candidates = perParentApplies.get(apply.parent) || []
 
-      let [applyCandidates, important] = extractApplyCandidates(apply.params)
+    perParentApplies.set(apply.parent, [candidates, apply.source])
 
-      if (apply.parent.type === 'atrule') {
-        if (apply.parent.name === 'screen') {
-          const screenType = apply.parent.params
+    let [applyCandidates, important] = extractApplyCandidates(apply.params)
 
-          throw apply.error(
-            `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates
-              .map((c) => `${screenType}:${c}`)
-              .join(' ')} instead.`
-          )
-        }
+    if (apply.parent.type === 'atrule') {
+      if (apply.parent.name === 'screen') {
+        const screenType = apply.parent.params
 
         throw apply.error(
-          `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`
+          `@apply is not supported within nested at-rules like @screen. We suggest you write this as @apply ${applyCandidates
+            .map((c) => `${screenType}:${c}`)
+            .join(' ')} instead.`
         )
       }
 
-      for (let applyCandidate of applyCandidates) {
-        if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
-          // TODO: Link to specific documentation page with error code.
-          throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
-        }
-
-        if (!applyClassCache.has(applyCandidate)) {
-          throw apply.error(
-            `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
-          )
-        }
+      throw apply.error(
+        `@apply is not supported within nested at-rules like @${apply.parent.name}. You can fix this by un-nesting @${apply.parent.name}.`
+      )
+    }
 
-        let rules = applyClassCache.get(applyCandidate)
+    for (let applyCandidate of applyCandidates) {
+      if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
+        // TODO: Link to specific documentation page with error code.
+        throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
+      }
 
-        candidates.push([applyCandidate, important, rules])
+      if (!applyClassCache.has(applyCandidate)) {
+        throw apply.error(
+          `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
+        )
       }
+
+      let rules = applyClassCache.get(applyCandidate)
+
+      candidates.push([applyCandidate, important, rules])
     }
+  }
+
+  for (const [parent, [candidates, atApplySource]] of perParentApplies) {
+    let siblings = []
+
+    for (let [applyCandidate, important, rules] of candidates) {
+      for (let [meta, node] of rules) {
+        let parentClasses = extractClasses(parent)
+        let nodeClasses = extractClasses(node)
+
+        // Add base utility classes from the @apply node to the list of
+        // classes to check whether it intersects and therefore results in a
+        // circular dependency or not.
+        //
+        // E.g.:
+        // .foo {
+        //   @apply hover:a; // This applies "a" but with a modifier
+        // }
+        //
+        // We only have to do that with base classes of the `node`, not of the `parent`
+        // E.g.:
+        // .hover\:foo {
+        //   @apply bar;
+        // }
+        // .bar {
+        //   @apply foo;
+        // }
+        //
+        // This should not result in a circular dependency because we are
+        // just applying `.foo` and the rule above is `.hover\:foo` which is
+        // unrelated. However, if we were to apply `hover:foo` then we _did_
+        // have to include this one.
+        nodeClasses = nodeClasses.concat(
+          extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)
+        )
 
-    for (const [parent, [candidates, atApplySource]] of perParentApplies) {
-      let siblings = []
-
-      for (let [applyCandidate, important, rules] of candidates) {
-        for (let [meta, node] of rules) {
-          let parentClasses = extractClasses(parent)
-          let nodeClasses = extractClasses(node)
-
-          // Add base utility classes from the @apply node to the list of
-          // classes to check whether it intersects and therefore results in a
-          // circular dependency or not.
-          //
-          // E.g.:
-          // .foo {
-          //   @apply hover:a; // This applies "a" but with a modifier
-          // }
-          //
-          // We only have to do that with base classes of the `node`, not of the `parent`
-          // E.g.:
-          // .hover\:foo {
-          //   @apply bar;
-          // }
-          // .bar {
-          //   @apply foo;
-          // }
-          //
-          // This should not result in a circular dependency because we are
-          // just applying `.foo` and the rule above is `.hover\:foo` which is
-          // unrelated. However, if we were to apply `hover:foo` then we _did_
-          // have to include this one.
-          nodeClasses = nodeClasses.concat(
-            extractBaseCandidates(nodeClasses, context.tailwindConfig.separator)
+        let intersects = parentClasses.some((selector) => nodeClasses.includes(selector))
+        if (intersects) {
+          throw node.error(
+            `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`
           )
+        }
 
-          let intersects = parentClasses.some((selector) => nodeClasses.includes(selector))
-          if (intersects) {
-            throw node.error(
-              `You cannot \`@apply\` the \`${applyCandidate}\` utility here because it creates a circular dependency.`
-            )
-          }
+        let root = postcss.root({ nodes: [node.clone()] })
 
-          let root = postcss.root({ nodes: [node.clone()] })
+        // Make sure every node in the entire tree points back at the @apply rule that generated it
+        root.walk((node) => {
+          node.source = atApplySource
+        })
 
-          // Make sure every node in the entire tree points back at the @apply rule that generated it
-          root.walk((node) => {
-            node.source = atApplySource
-          })
+        let canRewriteSelector =
+          node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
+
+        if (canRewriteSelector) {
+          root.walkRules((rule) => {
+            // Let's imagine you have the following structure:
+            //
+            // .foo {
+            //   @apply bar;
+            // }
+            //
+            // @supports (a: b) {
+            //   .bar {
+            //     color: blue
+            //   }
+            //
+            //   .something-unrelated {}
+            // }
+            //
+            // In this case we want to apply `.bar` but it happens to be in
+            // an atrule node. We clone that node instead of the nested one
+            // because we still want that @supports rule to be there once we
+            // applied everything.
+            //
+            // However it happens to be that the `.something-unrelated` is
+            // also in that same shared @supports atrule. This is not good,
+            // and this should not be there. The good part is that this is
+            // a clone already and it can be safely removed. The question is
+            // how do we know we can remove it. Basically what we can do is
+            // match it against the applyCandidate that you want to apply. If
+            // it doesn't match the we can safely delete it.
+            //
+            // If we didn't do this, then the `replaceSelector` function
+            // would have replaced this with something that didn't exist and
+            // therefore it removed the selector altogether. In this specific
+            // case it would result in `{}` instead of `.something-unrelated {}`
+            if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) {
+              rule.remove()
+              return
+            }
 
-          let canRewriteSelector =
-            node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
-
-          if (canRewriteSelector) {
-            root.walkRules((rule) => {
-              // Let's imagine you have the following structure:
-              //
-              // .foo {
-              //   @apply bar;
-              // }
-              //
-              // @supports (a: b) {
-              //   .bar {
-              //     color: blue
-              //   }
-              //
-              //   .something-unrelated {}
-              // }
-              //
-              // In this case we want to apply `.bar` but it happens to be in
-              // an atrule node. We clone that node instead of the nested one
-              // because we still want that @supports rule to be there once we
-              // applied everything.
-              //
-              // However it happens to be that the `.something-unrelated` is
-              // also in that same shared @supports atrule. This is not good,
-              // and this should not be there. The good part is that this is
-              // a clone already and it can be safely removed. The question is
-              // how do we know we can remove it. Basically what we can do is
-              // match it against the applyCandidate that you want to apply. If
-              // it doesn't match the we can safely delete it.
-              //
-              // If we didn't do this, then the `replaceSelector` function
-              // would have replaced this with something that didn't exist and
-              // therefore it removed the selector altogether. In this specific
-              // case it would result in `{}` instead of `.something-unrelated {}`
-              if (!extractClasses(rule).some((candidate) => candidate === applyCandidate)) {
-                rule.remove()
-                return
-              }
-
-              rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
-
-              rule.walkDecls((d) => {
-                d.important = meta.important || important
-              })
-            })
-          }
+            rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
 
-          // Insert it
-          siblings.push([
-            // Ensure that when we are sorting, that we take the layer order into account
-            { ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
-            root.nodes[0],
-          ])
+            rule.walkDecls((d) => {
+              d.important = meta.important || important
+            })
+          })
         }
+
+        // Insert it
+        siblings.push([
+          // Ensure that when we are sorting, that we take the layer order into account
+          { ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
+          root.nodes[0],
+        ])
       }
+    }
 
-      // Inject the rules, sorted, correctly
-      let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
+    // Inject the rules, sorted, correctly
+    let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
 
-      // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
-      parent.after(nodes)
-    }
+    // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
+    parent.after(nodes)
+  }
 
-    for (let apply of applies) {
-      // If there are left-over declarations, just remove the @apply
-      if (apply.parent.nodes.length > 1) {
-        apply.remove()
-      } else {
-        // The node is empty, drop the full node
-        apply.parent.remove()
-      }
+  for (let apply of applies) {
+    // If there are left-over declarations, just remove the @apply
+    if (apply.parent.nodes.length > 1) {
+      apply.remove()
+    } else {
+      // The node is empty, drop the full node
+      apply.parent.remove()
     }
-
-    // Do it again, in case we have other `@apply` rules
-    processApply(root, context, localCache)
   }
+
+  // Do it again, in case we have other `@apply` rules
+  processApply(root, context, localCache)
 }
 
 export default function expandApplyAtRules(context) {

diff --git a/tests/prefix.test.js b/tests/prefix.test.js
--- a/tests/prefix.test.js
+++ b/tests/prefix.test.js
@@ -374,3 +374,29 @@ it('prefix does not detect and generate unnecessary classes', async () => {
 
   expect(result.css).toMatchFormattedCss(css``)
 })
+
+it('supports prefixed utilities using arbitrary values', async () => {
+  let config = {
+    prefix: 'tw-',
+    content: [{ raw: html`foo` }],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    .foo {
+      @apply tw-text-[color:rgb(var(--button-background,var(--primary-button-background)))];
+      @apply tw-ease-[cubic-bezier(0.77,0,0.175,1)];
+      @apply tw-rounded-[min(4px,var(--input-border-radius))];
+    }
+  `
+
+  const result = await run(input, config)
+
+  expect(result.css).toMatchFormattedCss(css`
+    .foo {
+      color: rgb(var(--button-background, var(--primary-button-background)));
+      transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
+      border-radius: min(4px, var(--input-border-radius));
+    }
+  `)
+})

Prefix option does not work with custom properties
**What version of Tailwind CSS are you using?**

v3.0.23

Hi !

I am trying to use the prefix option, but I have found a problem that prevents it to work for more complex patterns. For instance, here is a simple code _without_ any prefix:

```
.button {
  @apply text-[color:rgb(var(--button-background,var(--primary-button-background)))];
}
```

This properly generates the following markup:

```
.button {
  color: rgb(var(--button-background,var(--primary-button-background)));
}
```

When using a prefix (in my example "tw-"), if I change the apply:

```
.button {
    @apply tw-text-[color:rgb(var(--button-background,var(--primary-button-background)))];
  }
```

Then the following incorrect CSS is generated (note the lack of selector):

```
 {
  color: rgb(var(--button-background,var(--primary-button-background)));
}
```

To complement this issue, here are a few other use cases where CSS generation fails with prefix:

```
.accordion-content {
    @apply tw-ease-[cubic-bezier(0.77,0,0.175,1)];
  }
```

Generates this incorrect markup:

```
 {
  transition-timing-function: cubic-bezier(0.77,0,0.175,1);
}
```

While the same without the prefix configured generate the correct markup.
Hi,

Here are a few other cases where Tailwind generate incorrect CSS with prefix:

```
.foo {
  @apply tw-rounded-[min(4px,var(--input-border-radius))];
}
```

It generates the following incorrect code (notice the lack of selector):

```
{
  border-radius: min(4px,var(--input-border-radius));
}
```
I'd be in favor to, since Bootstrap can live beside other libraries (eg. Booster v4 includes jQuery TableSorter, speaking of table styles leaking) or custom code.

[GarageBand For Android
](https://www.garagebandapp.org/)
tests/prefix.test.js