Repository: less/less.js (2025-07-21T23:16:15Z)

Issue:
Dotted layer name in `@layer` rule throws an error in Less 4.4.0 (regression)
**To reproduce:**

👉️ [Playground link](https://lesscss.org/less-preview/#eyJjb2RlIjoiQGxheWVyIGZvby5iYXoge1xuICAgLmJhciB7XG4gICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gICB9XG59IiwiYWN0aXZlVmVyc2lvbiI6IjQuNC4wIiwibWF0aCI6InBhcmVucy1kaXZpc2lvbiIsInN0cmljdFVuaXRzIjpmYWxzZX0=) 👈️

1. Write a CSS `@layer` rule in a `.less` stylesheet addressing a nested layer using dotted notation:

     ```less
    @layer foo.baz {
        .bar {
            font-weight: bold;
        }
    }
    ```
  
2. Process it:

    ```
    npx less stylesheet.less
    ```

3. Bang:

    > `Error: [less] at-rule options not recognised`


**Current behavior:**

`lessc` throws an error on the dotted syntax.

**Expected behavior:**

The dotted syntax should pass through `lessc` the same way as a non-dotted rule.

**Environment information:**

<!-- Provide the `less` and `nodejs` packages versions -->

- `less` version: 4.4.0
- `nodejs` version: 22.17.0 
- `operating system`: Linux


<!-- feel free to add additional comments -->

### Other links

[MDN Link about nested CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer#nesting_layers)

[Playground example of a non-dotted layer name working great in 4.4.0](https://lesscss.org/less-preview/#eyJjb2RlIjoiQGxheWVyIGZvbyB7XG4gICAuYmFyIHtcbiAgICAgICBmb250LXdlaWdodDogYm9sZDtcbiAgIH1cbn0iLCJhY3RpdmVWZXJzaW9uIjoiNC40LjAiLCJtYXRoIjoicGFyZW5zLWRpdmlzaW9uIiwic3RyaWN0VW5pdHMiOmZhbHNlfQ==)

[Playground example of the dotted notation working in 4.3.0](https://lesscss.org/less-preview/#eyJjb2RlIjoiQGxheWVyIGZvby5iYXoge1xuICAgLmJhciB7XG4gICAgICAgZm9udC13ZWlnaHQ6IGJvbGQ7XG4gICB9XG59IiwiYWN0aXZlVmVyc2lvbiI6IjQuMy4wIiwibWF0aCI6InBhcmVucy1kaXZpc2lvbiIsInN0cmljdFVuaXRzIjpmYWxzZX0=)



Discussion:
<!-- Greeting -->
Hi @n3dst4! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the less.js team.

<!-- Answer -->
Less 4.4.0 does not support dotted layer names in the `@layer` rule. The parser expects a simple identifier and rejects dotted names, which causes the "at-rule options not recognised" error. While support for the `@layer` rule and layer import syntax was added recently, there is no mention of support for dotted or nested layer names, and no workarounds or plans for this feature are documented or discussed in the codebase or docs [[parser source](https://github.com/less/less.js/blob/67404cd50c0582e2cec87daef6ea2629a319d166/packages/less/src/less/parser/parser.js)].

At this time, Less will not pass through valid CSS with dotted layer names unmodified. If you need this syntax, you may need to preprocess your CSS or wait for future updates that expand support for the full CSS `@layer` spec.


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot).*

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/eb2fbcfd-7e61-4cf6-bb97-2c3f7532a85b?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/less/less.js/issues/4348)
@n3dst4 Hello, thank you for reporting the issue. I've prepared a fix in this PR: https://github.com/less/less.js/pull/4349
Release PR: https://github.com/less/less.js/pull/4351



Patch:
diff --git a/packages/less/src/less/parser/parser.js b/packages/less/src/less/parser/parser.js
index 3fe7a37f5..f1bc2d32a 100644
--- a/packages/less/src/less/parser/parser.js
+++ b/packages/less/src/less/parser/parser.js
@@ -2048,7 +2048,49 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
                     return null;
                 }
             },
-
+            atruleUnknown: function (value, name, hasBlock) {
+                value = this.permissiveValue(/^[{;]/);
+                hasBlock = (parserInput.currentChar() === '{');
+                if (!value) {
+                    if (!hasBlock && parserInput.currentChar() !== ';') {
+                        error(''.concat(name, ' rule is missing block or ending semi-colon'));
+                    }
+                }
+                else if (!value.value) {
+                    value = null;
+                }
+                return [value, hasBlock];
+            },
+            atruleBlock: function (rules, value, isRooted, isKeywordList) {
+                rules = this.blockRuleset();
+                parserInput.save();
+                if (!rules && !isRooted) {
+                    value = this.entity();
+                    rules = this.blockRuleset();
+                }
+                if (!rules && !isRooted) {
+                    parserInput.restore();
+                    var e = [];
+                    value = this.entity();
+                    while (parserInput.$char(',')) {
+                        e.push(value);
+                        value = this.entity();
+                    }
+                    if (value && e.length > 0) {
+                        e.push(value);
+                        value = e;
+                        isKeywordList = true;
+                    }
+                    else {
+                        rules = this.blockRuleset();
+                    }
+                }
+                else {
+                    parserInput.forget();
+                }
+                    
+                return [rules, value, isKeywordList];
+            },
             //
             // A CSS AtRule
             //
@@ -2127,48 +2169,29 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
                         error(`expected ${name} expression`);
                     }
                 } else if (hasUnknown) {
-                    value = this.permissiveValue(/^[{;]/);
-                    hasBlock = (parserInput.currentChar() === '{');
-                    if (!value) {
-                        if (!hasBlock && parserInput.currentChar() !== ';') {
-                            error(`${name} rule is missing block or ending semi-colon`);
-                        }
-                    }
-                    else if (!value.value) {
-                        value = null;
-                    }
+                    const unknownPackage = this.atruleUnknown(value, name, hasBlock);
+                    value = unknownPackage[0];
+                    hasBlock = unknownPackage[1];
                 }
-
+                    
                 if (hasBlock) {
-                    rules = this.blockRuleset();
-
-                    parserInput.save();
-
-                    if (!rules && !isRooted) {
-                        value = this.entity();
-                        rules = this.blockRuleset();
-                    }
+                    let blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
+                    rules = blockPackage[0];
+                    value = blockPackage[1];
+                    isKeywordList = blockPackage[2];
 
-                    if (!rules && !isRooted) {
+                    if (!rules && !hasUnknown) {
                         parserInput.restore();
-
-                        let e = [];
-                        value = this.entity();
-
-                        while (parserInput.$char(',')) {
-                            e.push(value);
-                            value = this.entity();
+                        name = parserInput.$re(/^@[a-z-]+/);
+                        const unknownPackage = this.atruleUnknown(value, name, hasBlock);
+                        value = unknownPackage[0];
+                        hasBlock = unknownPackage[1];
+                        if (hasBlock) {
+                            blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
+                            rules = blockPackage[0];
+                            value = blockPackage[1];
+                            isKeywordList = blockPackage[2];
                         }
-
-                        if (value && e.length > 0) {
-                            e.push(value);
-                            value = e;
-                            isKeywordList = true;
-                        } else {
-                            rules = this.blockRuleset();
-                        }
-                    } else {
-                        parserInput.forget();
                     }
                 }
 


Test patch:
diff --git a/packages/test-data/css/_main/layer.css b/packages/test-data/css/_main/layer.css
index 43218ad0a..7196325af 100644
--- a/packages/test-data/css/_main/layer.css
+++ b/packages/test-data/css/_main/layer.css
@@ -69,3 +69,25 @@
 .parent:hover {
   background: lightgray;
 }
+@layer foo.baz {
+  .bar {
+    font-weight: bold;
+  }
+}
+@layer framework {
+  @layer layout {
+    .container {
+      display: grid;
+      gap: 2rem;
+    }
+  }
+}
+@layer framework.layout {
+  main {
+    padding: 2rem;
+  }
+  p {
+    margin-block: 1rem;
+    color: #555;
+  }
+}
diff --git a/packages/test-data/less/_main/layer.less b/packages/test-data/less/_main/layer.less
index 8e93ec6b9..9340f83af 100644
--- a/packages/test-data/less/_main/layer.less
+++ b/packages/test-data/less/_main/layer.less
@@ -85,3 +85,28 @@
         background: lightgray;
     }
 }
+
+@layer foo.baz {
+   .bar {
+       font-weight: bold;
+   }
+}
+
+@layer framework {
+    @layer layout {
+        .container {
+            display: grid;
+            gap: 2rem;
+        }
+    }
+}
+
+@layer framework.layout {
+    main {
+        padding: 2rem;
+    }
+    p {
+        margin-block: 1rem;
+        color: #555;
+    }
+}