Skip to content

Bug: use-layers reports keyframe blocks as rules that need to be in a layer #523

Description

@minseonkkim

Environment

ESLint version: v10.7.0
@eslint/css version: v1.4.0
Node version: v22.15.0
npm version: v11.12.1
Operating System: Windows 11

Which language are you using?

stylesheet

What did you do?

I enabled use-layers while adopting cascade layers in a project that contains CSS animations, and every @keyframes block immediately produced errors on its keyframe steps.

Configuration
import css from "@eslint/css";

export default [
    {
        files: ["**/*.css"],
        plugins: { css },
        language: "css/css",
        rules: {
            "css/use-layers": "error",
        },
    },
];
@keyframes spin {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}

The from / to form and vendor-prefixed keyframes (@-webkit-keyframes) produce the same errors.

What did you expect to happen?

No errors. Keyframe steps (0%, 100%, from, to) are not style rules that participate in the cascade — they are keyframe selectors inside an at-rule — so requiring them to be "within a layer" doesn't apply to them.

Wrapping the @keyframes in @layer does silence the errors, but that's not a real resolution: @keyframes has no relationship to cascade layers, so the rule effectively forces authors to either wrap every animation in a layer or disable the rule around keyframes.

What actually happened?

Each keyframe step is reported as a rule that should be within a layer:

2:3  error  Expected rule to be within a layer  css/use-layers
5:3  error  Expected rule to be within a layer  css/use-layers

Root cause: css-tree parses keyframe steps as Rule nodes, and the Rule handler in src/rules/use-layers.js reports any Rule whenever layerDepth is 0, without checking whether the node is inside a @keyframes block:

Rule(node) {
    if (layerDepth > 0) {
        return;
    }

    context.report({
        loc: node.loc,
        messageId: "missingLayer",
    });
},

A possible fix is to track keyframes the same way no-duplicate-keyframe-selectors does - an Atrule[name=/^(-(o|moz|webkit)-)?keyframes$/i] enter/exit pair maintaining a keyframesDepth, and an early return in the Rule handler when inside one. Rules nested in other at-rules (@media, @supports) should still be reported, since those do participate in the cascade.

Link to Minimal Reproducible Example

https://stackblitz.com/edit/stackblitz-starters-ru1f8tzx?description=Starter%20project%20for%20Node.js,%20a%20JavaScript%20runtime%20built%20on%20Chrome%27s%20V8%20JavaScript%20engine&file=eslint.config.mjs,test.css&title=node.new%20Starter

Participation

  • I am willing to submit a pull request for this issue.

AI acknowledgment

  • I did not use AI to generate this issue report.
  • (If the above is not checked) I have reviewed the AI-generated content before submitting.

Additional comments

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Feedback Needed

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions