Repository: tailwindlabs/tailwindcss

Problem statement:
Safari ignores alpha channel in colors in `::marker`, which makes styling them with Tailwind impossible
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of Tailwind CSS are you using?**

v3.1.2

**What build tool (or framework if it abstracts the build tool) are you using?**

<details>
<summary>`postcss`</summary>

```js
module.exports = {
  plugins: {
    "postcss-import": {},
    "tailwindcss": {},
    "autoprefixer": {},
  }
}
```
</details>

**What version of Node.js are you using?**

v12.22.9

**What browser are you using?**

Safari 15.5 on macOS 12.4

**Reproduction URL**

https://play.tailwindcss.com/iea3Szbz1t

**Describe your issue**

On Safari, all of the following examples work correctly:

```css
::marker {
  color: red;
}
::marker {
  color: #f00;
}
::marker {
  color: rgb(255 0 0);
}
```

But these are completely ignored:

```css
::marker {
  color: rgba(255 0 0 50);
}
::marker {
  color: rgb(255 0 0 / 50);
}
```

I discovered this while playing with arbitrary variants, trying to lighten the ordinals in an ordered list. When that didn't work, I thought, "that's fine, I can just use an arbitrary value!" but turns out that `[&::marker]:text-[#ff0000]` gets translated to

```css
.\[\&\:\:marker\]\:text-\[\#ff0000\]::marker {
  --tw-text-opacity: 1;
  color: rgb(255 0 0 / var(--tw-text-opacity));
}
```

Which makes it impossible to use Tailwind to style marker colors.

I realize this is more of a Safari bug than a Tailwind bug, but I'm curious as to why you're translating a hardcoded `[red]` into an RGBA value, and whether that's something that could be addressed to allow styling list markers with Tailwind 🤔


Hints:
For anyone else that stumbles into this, the workaround I've found is to add this to my CSS file:

``` css
::marker {
  color: theme(colors.slate.500);
}
```

But ideally this should be handled from the HTML to make it easier to manage together with the other styles.
Looked at this quickly, it seems like the thing that breaks it is the variable, not the fact that there's an opacity value set:

https://jsfiddle.net/oyv60qhd/1/

So you can work around this by disabling all of our color opacity utilities like this:

```js
// tailwind.config.js
module.exports = {
  // ...
  corePlugins: {
    textOpacity: false,
    backgroundOpacity: false,
    divideOpacity: false,
    borderOpacity: false,
    placeholderOpacity: false,
  },
}
```

Those will end up being disabled by default in v4.

The whole reason we generate CSS like this:

```css
.\[\&\:\:marker\]\:text-\[\#ff0000\]::marker {
  --tw-text-opacity: 1;
  color: rgb(255 0 0 / var(--tw-text-opacity));
}
```

...is so you can combine the color with the corresponding opacity utility, for example:

```html
<li class="marker:text-[red] marker:text-opacity-50">
```

...but generally we encourage the opacity modifier now, which doesn't require the variable but we still have to output it for backwards compatibility reasons (unless those opacity plugins are disabled).

Will leave this open at least for now though so I remember to discuss it with the team in case there's an easy way to just make this work that isn't a breaking change.
I've submitted a WebKit bug report for this: [WebKit Bug #241576](https://bugs.webkit.org/show_bug.cgi?id=241576)
hah I already did that earlier today: https://bugs.webkit.org/show_bug.cgi?id=241566 — in any case appreciate it!
The problem is that custom properties are not supported because allowed properties in `::marker` are whitelisted in webkit (correct): https://github.com/WebKit/WebKit/blob/fdc6c5fc1872fab62106b209c8a6a5aaa00730be/Source/WebCore/style/PropertyAllowlist.cpp

but they don't include custom properties (incorrect). The spec doesn't mention them though so it's also possibly a CSS spec issue.

Patch:
diff --git a/src/corePlugins.js b/src/corePlugins.js
--- a/src/corePlugins.js
+++ b/src/corePlugins.js
@@ -14,6 +14,7 @@ import { version as tailwindVersion } from '../package.json'
 import log from './util/log'
 import { normalizeScreens } from './util/normalizeScreens'
 import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadowValue'
+import { removeAlphaVariables } from './util/removeAlphaVariables'
 import { flagEnabled } from './featureFlags'
 
 export let variantPlugins = {
@@ -21,7 +22,19 @@ export let variantPlugins = {
     addVariant('first-letter', '&::first-letter')
     addVariant('first-line', '&::first-line')
 
-    addVariant('marker', ['& *::marker', '&::marker'])
+    addVariant('marker', [
+      ({ container }) => {
+        removeAlphaVariables(container, ['--tw-text-opacity'])
+
+        return '& *::marker'
+      },
+      ({ container }) => {
+        removeAlphaVariables(container, ['--tw-text-opacity'])
+
+        return '&::marker'
+      },
+    ])
+
     addVariant('selection', ['& *::selection', '&::selection'])
 
     addVariant('file', '&::file-selector-button')
@@ -77,21 +90,11 @@ export let variantPlugins = {
       [
         'visited',
         ({ container }) => {
-          let toRemove = ['--tw-text-opacity', '--tw-border-opacity', '--tw-bg-opacity']
-
-          container.walkDecls((decl) => {
-            if (toRemove.includes(decl.prop)) {
-              decl.remove()
-
-              return
-            }
-
-            for (const varName of toRemove) {
-              if (decl.value.includes(`/ var(${varName})`)) {
-                decl.value = decl.value.replace(`/ var(${varName})`, '')
-              }
-            }
-          })
+          removeAlphaVariables(container, [
+            '--tw-text-opacity',
+            '--tw-border-opacity',
+            '--tw-bg-opacity',
+          ])
 
           return '&:visited'
         },
diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js
--- a/src/lib/generateRules.js
+++ b/src/lib/generateRules.js
@@ -163,15 +163,17 @@ function applyVariant(variant, matches, context) {
 
       let container = postcss.root({ nodes: [rule.clone()] })
 
-      for (let [variantSort, variantFunction] of variantFunctionTuples) {
-        let clone = container.clone()
+      for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) {
+        let clone = containerFromArray ?? container.clone()
         let collectedFormats = []
 
-        let originals = new Map()
-
         function prepareBackup() {
-          if (originals.size > 0) return // Already prepared, chicken out
-          clone.walkRules((rule) => originals.set(rule, rule.selector))
+          // Already prepared, chicken out
+          if (clone.raws.neededBackup) {
+            return
+          }
+          clone.raws.neededBackup = true
+          clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector))
         }
 
         function modifySelectors(modifierFunction) {
@@ -231,6 +233,10 @@ function applyVariant(variant, matches, context) {
               // reserving additional X places for these 'unknown' variants in between.
               variantSort | BigInt(idx << ruleWithVariant.length),
               variantFunction,
+
+              // If the clone has been modified we have to pass that back
+              // though so each rule can use the modified container
+              clone.clone(),
             ])
           }
           continue
@@ -244,13 +250,15 @@ function applyVariant(variant, matches, context) {
           continue
         }
 
-        // We filled the `originals`, therefore we assume that somebody touched
+        // We had to backup selectors, therefore we assume that somebody touched
         // `container` or `modifySelectors`. Let's see if they did, so that we
         // can restore the selectors, and collect the format strings.
-        if (originals.size > 0) {
+        if (clone.raws.neededBackup) {
+          delete clone.raws.neededBackup
           clone.walkRules((rule) => {
-            if (!originals.has(rule)) return
-            let before = originals.get(rule)
+            let before = rule.raws.originalSelector
+            if (!before) return
+            delete rule.raws.originalSelector
             if (before === rule.selector) return // No mutation happened
 
             let modified = rule.selector
diff --git a/src/util/removeAlphaVariables.js b/src/util/removeAlphaVariables.js
new file mode 100644
--- /dev/null
+++ b/src/util/removeAlphaVariables.js
@@ -0,0 +1,24 @@
+/**
+ * This function removes any uses of CSS variables used as an alpha channel
+ *
+ * This is required for selectors like `:visited` which do not allow
+ * changes in opacity or external control using CSS variables.
+ *
+ * @param {import('postcss').Container} container
+ * @param {string[]} toRemove
+ */
+export function removeAlphaVariables(container, toRemove) {
+  container.walkDecls((decl) => {
+    if (toRemove.includes(decl.prop)) {
+      decl.remove()
+
+      return
+    }
+
+    for (let varName of toRemove) {
+      if (decl.value.includes(`/ var(${varName})`)) {
+        decl.value = decl.value.replace(`/ var(${varName})`, '')
+      }
+    }
+  })
+}


Test patch:
diff --git a/tests/parallel-variants.test.js b/tests/parallel-variants.test.js
--- a/tests/parallel-variants.test.js
+++ b/tests/parallel-variants.test.js
@@ -85,3 +85,52 @@ test('parallel variants can be generated using a function that returns parallel
     `)
   })
 })
+
+test('a function that returns parallel variants can modify the container', async () => {
+  let config = {
+    content: [
+      {
+        raw: html`<div
+          class="hover:test:font-black test:font-bold test:font-medium font-normal"
+        ></div>`,
+      },
+    ],
+    plugins: [
+      function test({ addVariant }) {
+        addVariant('test', ({ container }) => {
+          container.walkDecls((decl) => {
+            decl.value = `calc(0 + ${decl.value})`
+          })
+
+          return ['& *::test', '&::test']
+        })
+      },
+    ],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .font-normal {
+        font-weight: 400;
+      }
+      .test\:font-bold *::test {
+        font-weight: calc(0 + 700);
+      }
+      .test\:font-medium *::test {
+        font-weight: calc(0 + 500);
+      }
+      .test\:font-bold::test {
+        font-weight: calc(0 + 700);
+      }
+      .test\:font-medium::test {
+        font-weight: calc(0 + 500);
+      }
+      .hover\:test\:font-black *:hover::test {
+        font-weight: calc(0 + 900);
+      }
+      .hover\:test\:font-black:hover::test {
+        font-weight: calc(0 + 900);
+      }
+    `)
+  })
+})
diff --git a/tests/variants.test.css b/tests/variants.test.css
--- a/tests/variants.test.css
+++ b/tests/variants.test.css
@@ -112,16 +112,14 @@
   line-height: 1.75rem;
 }
 .marker\:text-red-500 *::marker {
-  --tw-text-opacity: 1;
-  color: rgb(239 68 68 / var(--tw-text-opacity));
+  color: rgb(239 68 68);
 }
 .marker\:text-lg::marker {
   font-size: 1.125rem;
   line-height: 1.75rem;
 }
 .marker\:text-red-500::marker {
-  --tw-text-opacity: 1;
-  color: rgb(239 68 68 / var(--tw-text-opacity));
+  color: rgb(239 68 68);
 }
 .selection\:bg-blue-500 *::selection {
   --tw-bg-opacity: 1;
diff --git a/tests/variants.test.js b/tests/variants.test.js
--- a/tests/variants.test.js
+++ b/tests/variants.test.js
@@ -485,8 +485,7 @@ test('returning non-strings and non-selectors in addVariant', () => {
 
         addVariant('peer-aria-expanded-2', ({ modifySelectors, separator }) => {
           let nodes = modifySelectors(
-            ({ className }) =>
-              `.peer[aria-expanded="false"] ~ .${e(`peer-aria-expanded${separator}${className}`)}`
+            ({ className }) => `.${e(`peer-aria-expanded-2${separator}${className}`)}`
           )
 
           return [