Repository: tailwindlabs/tailwindcss
Pull request #2312 (2020-09-04 16:19:23)

Move custom CSS within layers to corresponding Tailwind layer
This PR takes any custom CSS defined within a `@layer` at-rule that matches one of Tailwind's layers and groups them together with the corresponding Tailwind rules.

For example, this CSS:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn { background: blue }
}

@layer utilities {
  .align-banana { text-align: banana }
}

@layer base {
  h1 { font-weight: bold }
}

@layer components {
  .card { border-radius: 12px }
}

@layer base {
  p { font-weight: normal }
}

@layer utilities {
  .align-sandwich { text-align: sandwich }
}
```

...conceptually becomes this:

```css
@tailwind base;
h1 { font-weight: bold }
p { font-weight: normal }

@tailwind components;
.btn { background: blue }
.card { border-radius: 12px }

@tailwind utilities;
.align-banana { text-align: banana }
.align-sandwich { text-align: sandwich }
```

This is useful for avoiding CSS declaration order specificity issues and lets you decouple authoring order from intended render order. This is generally inline with the new `@layer` CSS proposal that came out shortly after we introduced this at-rule to Tailwind ourselves without knowing that existed, heh.

This is a change in behavior to how things worked before but we don't document the `@layer` rule outside of mentioning it in the release notes for Tailwind 1.6. I am going to be a bit bold and classify this as a bugfix, as this is how this always should have worked.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 107d0433..7f2b779d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - New `place-items`, `place-content`, `place-self`, `justify-items`, and `justify-self` utilities ([#2306](https://github.com/tailwindlabs/tailwindcss/pull/2306))
 - Support configuring variants as functions ([#2309](https://github.com/tailwindlabs/tailwindcss/pull/2309))
 
+### Changed
+
+- CSS within `@layer` at-rules are now grouped with the corresponding `@tailwind` at-rule ([#2312](https://github.com/tailwindlabs/tailwindcss/pull/2312))
+
 ### Deprecated
 
 - `conservative` purge mode, deprecated in favor of `layers`
diff --git a/src/lib/convertLayerAtRulesToControlComments.js b/src/lib/convertLayerAtRulesToControlComments.js
index d2bcf0a7..e7ecc0ef 100644
--- a/src/lib/convertLayerAtRulesToControlComments.js
+++ b/src/lib/convertLayerAtRulesToControlComments.js
@@ -4,6 +4,11 @@ export default function convertLayerAtRulesToControlComments() {
   return function(css) {
     css.walkAtRules('layer', atRule => {
       const layer = atRule.params
+
+      if (!['base', 'components', 'utilities'].includes(layer)) {
+        return
+      }
+
       atRule.before(postcss.comment({ text: `tailwind start ${layer}` }))
       atRule.before(atRule.nodes)
       atRule.before(postcss.comment({ text: `tailwind end ${layer}` }))
diff --git a/src/lib/substituteTailwindAtRules.js b/src/lib/substituteTailwindAtRules.js
index ef9d36bf..759b8100 100644
--- a/src/lib/substituteTailwindAtRules.js
+++ b/src/lib/substituteTailwindAtRules.js
@@ -41,6 +41,19 @@ export default function(
     })
 
     let includesScreensExplicitly = false
+    const layers = {
+      base: [],
+      components: [],
+      utilities: [],
+    }
+
+    css.walkAtRules('layer', atRule => {
+      if (!['base', 'components', 'utilities'].includes(atRule.params)) {
+        return
+      }
+
+      layers[atRule.params].push(atRule)
+    })
 
     css.walkAtRules('tailwind', atRule => {
       if (atRule.params === 'preflight') {
@@ -49,14 +62,17 @@ export default function(
       }
 
       if (atRule.params === 'base') {
+        atRule.after(layers.base)
         atRule.after(updateSource(pluginBase, atRule.source))
       }
 
       if (atRule.params === 'components') {
+        atRule.after(layers.components)
         atRule.after(updateSource(pluginComponents, atRule.source))
       }
 
       if (atRule.params === 'utilities') {
+        atRule.after(layers.utilities)
         atRule.after(updateSource(pluginUtilities, atRule.source))
       }
 


Test patch:
diff --git a/__tests__/layerAtRule.test.js b/__tests__/layerAtRule.test.js
new file mode 100644
index 00000000..1653bf76
--- /dev/null
+++ b/__tests__/layerAtRule.test.js
@@ -0,0 +1,93 @@
+import postcss from 'postcss'
+import tailwind from '../src/index'
+
+function run(input, config = {}) {
+  return postcss([tailwind({ corePlugins: [], ...config })]).process(input, { from: undefined })
+}
+
+test('layers are grouped and inserted at the matching @tailwind rule', () => {
+  const input = `
+    @layer vanilla {
+      strong { font-weight: medium }
+    }
+
+    @tailwind base;
+    @tailwind components;
+    @tailwind utilities;
+
+    @layer components {
+      .btn { background: blue }
+    }
+
+    @layer utilities {
+      .align-banana { text-align: banana }
+    }
+
+    @layer base {
+      h1 { font-weight: bold }
+    }
+
+    @layer components {
+      .card { border-radius: 12px }
+    }
+
+    @layer base {
+      p { font-weight: normal }
+    }
+
+    @layer utilities {
+      .align-sandwich { text-align: sandwich }
+    }
+
+    @layer chocolate {
+      a { text-decoration: underline }
+    }
+  `
+
+  const expected = `
+    @layer vanilla {
+      strong { font-weight: medium }
+    }
+
+    body { margin: 0 }
+    h1 { font-weight: bold }
+    p { font-weight: normal }
+
+    .input { background: white }
+    .btn { background: blue }
+    .card { border-radius: 12px }
+
+    .float-squirrel { float: squirrel }
+    .align-banana { text-align: banana }
+    .align-sandwich { text-align: sandwich }
+
+    @layer chocolate {
+      a { text-decoration: underline }
+    }
+  `
+
+  expect.assertions(2)
+
+  return run(input, {
+    plugins: [
+      function({ addBase, addComponents, addUtilities }) {
+        addBase({
+          body: {
+            margin: 0,
+          },
+        })
+
+        addComponents({
+          '.input': { background: 'white' },
+        })
+
+        addUtilities({
+          '.float-squirrel': { float: 'squirrel' },
+        })
+      },
+    ],
+  }).then(result => {
+    expect(result.css).toMatchCss(expected)
+    expect(result.warnings().length).toBe(0)
+  })
+})