Repository: tailwindlabs/tailwindcss

Problem statement:
Order of CSS selectors is not correct with v3.2.x in specific scenarios
**What version of Tailwind CSS are you using?**

v3.2.4

**What build tool (or framework if it abstracts the build tool) are you using?**

I use Vite. But I was able to reproduce the bug with `npx tailwindcss -i ./src/style.css -o ./dist/style.css`.

**What version of Node.js are you using?**

v16.16.0

**What browser are you using?**

Chrome

**What operating system are you using?**

Windows 10

**Reproduction URL**

https://replit.com/@rahulv3a/Tailwind-bug. Please click on `Run` to run the instance and `Show files` to view the code. 

I couldn't replicate it on play.tailwindcss.com because the files are required to be in a specific folder structure to reproduce.

**Describe your issue**

I have a PHP project with hundreds of files. Markup of various components (like menus) is generated by a CMS. As adding classes is not an option, I need to use `[&_]` on the containers to style them. Some layouts break when I update from `v3.1.x` to `v3.2.x` because the CSS order of certain selectors in the CSS generated is wrong. 

It occurs only when:
- `[&_]` classes are used with `v3.2.x`. Everything works flawlessly with `v3.1.x`.
- specific conditions are met. I'm still trying to figure them out. But I was able to create an isolated demo for you.

**Example**

In the following markup, the list items should be stacked in SM, and side-by-side from MD onwards. But they stay stacked in all viewports.

```html
<div class="[&_ul]:flex [&_ul]:flex-col md:[&_ul]:flex-row">
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
</div>
```

Generated CSS:

```css
@media (min-width: 768px) {
  .md\:\[\&_ul\]\:flex-row ul {
    flex-direction: row;
  }
}

.\[\&_ul\]\:flex ul {
  display: flex;
}

.\[\&_ul\]\:flex-col ul {
  flex-direction: column;
}
```

Expected CSS:

```css
.\[\&_ul\]\:flex ul {
  display: flex;
}

.\[\&_ul\]\:flex-col ul {
  flex-direction: column;
}

@media (min-width: 768px) {
  .md\:\[\&_ul\]\:flex-row ul {
    flex-direction: row;
  }
}
```

**Demo**

I created an online demo https://replit.com/@rahulv3a/Tailwind-bug of the example mentioned above. It contains only the code required to reproduce the bug. Please click on `Run` to run the instance and `Show files` to view the code. 

- Tailwind setup
  - v3.2.4 is being used.
  - `yarn tailwindcss -i ./src/style.css -o ./dist/style.css` is used to generate the CSS.
  - `preflight` is disabled to keep things simple.

- Conditions
  - tailwind.config.js
    - `content` has `["./inc/**/*.php", "./templates/**/*.php"]`.
    - The order of the paths is important to replicate the issue.
  - inc/functions.php
    - It contains two comments. In my project, `./inc` contains hundreds of functions that are documented in detail. The comments contain words like contents, lowercase, etc. Tailwind creates classes for them which is fine. But for some reason unknown, the comments that start with `//` interfere with the order of CSS selectors.
  - template/01-required.php
    - I don't know why but this code is required to replicate the issue.
  - template/component.php
    - This file contains the actual code mentioned in the example above.

**More examples**
Here are some more examples of the bug I experience in my project with `v3.2.x`:
- `<br>` stays hidden in all viewports.
  ```html
  <h1 class="[&amp;_br]:hidden [&amp;_br]:xl:block">
    Lorem<br />
    Ipsum
  </h1>
  ```
- `<img>` stays full width in all viewports.
  ```html
  <figure class="[&_img]:w-full md:[&_img]:w-[300px] xl:[&_img]:w-[340px]">
    <img src="./image.jpg" alt="some image" />
  </figure>
  ```
I can create isolated demos for each of them if it helps you debug.

Please let me know if you need any other information.


Hints:
Thanks for reporting, I was able to distill the reproduction down to this minimal demo on Tailwind Play:

https://play.tailwindcss.com/3ujiz5LanM

Can see the sorting issue in the "Generated CSS" tab. Will look at this one next week!
Thanks for the reporting...  

Patch:
diff --git a/src/lib/offsets.js b/src/lib/offsets.js
--- a/src/lib/offsets.js
+++ b/src/lib/offsets.js
@@ -13,6 +13,7 @@ import { remapBitfield } from './remap-bitfield.js'
  * @property {function | undefined} sort The sort function
  * @property {string|null} value The value we want to compare
  * @property {string|null} modifier The modifier that was used (if any)
+ * @property {bigint} variant The variant bitmask
  */
 
 /**
@@ -127,6 +128,8 @@ export class Offsets {
    * @returns {RuleOffset}
    */
   applyVariantOffset(rule, variant, options) {
+    options.variant = variant.variants
+
     return {
       ...rule,
       layer: 'variants',
@@ -211,6 +214,19 @@ export class Offsets {
       for (let bOptions of b.options) {
         if (aOptions.id !== bOptions.id) continue
         if (!aOptions.sort || !bOptions.sort) continue
+
+        let maxFnVariant = max([aOptions.variant, bOptions.variant]) ?? 0n
+
+        // Create a mask of 0s from bits 1..N where N represents the mask of the Nth bit
+        let mask = ~(maxFnVariant | (maxFnVariant - 1n))
+        let aVariantsAfterFn = a.variants & mask
+        let bVariantsAfterFn = b.variants & mask
+
+        // If the variants the same, we _can_ sort them
+        if (aVariantsAfterFn !== bVariantsAfterFn) {
+          continue
+        }
+
         let result = aOptions.sort(
           {
             value: aOptions.value,


Test patch:
diff --git a/tests/arbitrary-variants.test.js b/tests/arbitrary-variants.test.js
--- a/tests/arbitrary-variants.test.js
+++ b/tests/arbitrary-variants.test.js
@@ -1158,3 +1158,201 @@ it('Invalid arbitrary variants selectors should produce nothing instead of faili
     expect(result.css).toMatchFormattedCss(css``)
   })
 })
+
+it('should output responsive variants + stacked variants in the right order', () => {
+  let config = {
+    content: [
+      {
+        raw: html`
+          <div class="xl:p-1"></div>
+          <div class="md:[&_ul]:flex-row"></div>
+          <div class="[&_ul]:flex"></div>
+          <div class="[&_ul]:flex-col"></div>
+        `,
+      },
+    ],
+    corePlugins: { preflight: false },
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      @media (min-width: 1280px) {
+        .xl\:p-1 {
+          padding: 0.25rem;
+        }
+      }
+      .\[\&_ul\]\:flex ul {
+        display: flex;
+      }
+      .\[\&_ul\]\:flex-col ul {
+        flex-direction: column;
+      }
+      @media (min-width: 768px) {
+        .md\:\[\&_ul\]\:flex-row ul {
+          flex-direction: row;
+        }
+      }
+    `)
+  })
+})
+
+it('should sort multiple variant fns with normal variants between them', () => {
+  /** @type {string[]} */
+  let lines = []
+
+  for (let a of [1, 2]) {
+    for (let b of [2, 1]) {
+      for (let c of [1, 2]) {
+        for (let d of [2, 1]) {
+          for (let e of [1, 2]) {
+            lines.push(`<div class="fred${a}:qux-[${b}]:baz${c}:bar-[${d}]:foo${e}:p-1"></div>`)
+          }
+        }
+      }
+    }
+  }
+
+  // Fisher-Yates shuffle
+  for (let i = lines.length - 1; i > 0; i--) {
+    let j = Math.floor(Math.random() * i)
+    ;[lines[i], lines[j]] = [lines[j], lines[i]]
+  }
+
+  let config = {
+    content: [
+      {
+        raw: lines.join('\n'),
+      },
+    ],
+    corePlugins: { preflight: false },
+    plugins: [
+      function ({ addVariant, matchVariant }) {
+        addVariant('foo1', '&[data-foo=1]')
+        addVariant('foo2', '&[data-foo=2]')
+
+        matchVariant('bar', (value) => `&[data-bar=${value}]`, {
+          sort: (a, b) => b.value - a.value,
+        })
+
+        addVariant('baz1', '&[data-baz=1]')
+        addVariant('baz2', '&[data-baz=2]')
+
+        matchVariant('qux', (value) => `&[data-qux=${value}]`, {
+          sort: (a, b) => b.value - a.value,
+        })
+
+        addVariant('fred1', '&[data-fred=1]')
+        addVariant('fred2', '&[data-fred=2]')
+      },
+    ],
+  }
+
+  let input = css`
+    @tailwind utilities;
+  `
+
+  return run(input, config).then((result) => {
+    expect(result.css).toMatchFormattedCss(css`
+      .fred1\:qux-\[2\]\:baz1\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='1'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz1\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='1'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz1\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='1'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz1\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='1'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz2\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='2'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz2\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='2'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz2\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='2'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[2\]\:baz2\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='2'][data-qux='2'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz1\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='1'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz1\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='1'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz1\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='1'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz1\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='1'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz2\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='2'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz2\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='2'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz2\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='2'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred1\:qux-\[1\]\:baz2\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='2'][data-qux='1'][data-fred='1'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz1\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='1'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz1\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='1'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz1\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='1'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz1\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='1'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz2\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='2'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz2\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='2'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz2\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='2'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[2\]\:baz2\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='2'][data-qux='2'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz1\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='1'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz1\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='1'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz1\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='1'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz1\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='1'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz2\:bar-\[2\]\:foo1\:p-1[data-foo='1'][data-bar='2'][data-baz='2'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz2\:bar-\[2\]\:foo2\:p-1[data-foo='2'][data-bar='2'][data-baz='2'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz2\:bar-\[1\]\:foo1\:p-1[data-foo='1'][data-bar='1'][data-baz='2'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+      .fred2\:qux-\[1\]\:baz2\:bar-\[1\]\:foo2\:p-1[data-foo='2'][data-bar='1'][data-baz='2'][data-qux='1'][data-fred='2'] {
+        padding: 0.25rem;
+      }
+    `)
+  })
+})