# Migrating to 17.0.0 This release contains breaking changes. We've migrated to ECMAScript modules (ESM), and modernised dozens of rules related to CSS nesting, specificity and vendor prefixes. ## Breaking changes We've removed: - CommonJs Node.js API - `output` property in the Node.js API returned resolved object - support for Node.js less than 20.19.0 - GitHub formatter - `resolveNestedSelectors` option from `selector-class-pattern` - `checkContextFunctionalPseudoClasses` option from `selector-max-id` And changed: - default `fix` mode to `strict` - report to be consistent and predictable in how it handles the provided position arguments - `selector-max-*` syntax rules for standard CSS nesting and modern functional pseudo-classes - `*-specificity` semantic rules for standard CSS nesting - `no-duplicate-selectors` and `selector-no-qualifying-type` for standard CSS nesting - `*-list` rules to have consistent behaviour for vendor prefixes and case - `*-no-vendor-prefix` rules to have consistent behaviour for their `ignore*: []` secondary options - `declaration-property-max-values` rule to have consistent behaviour for vendor prefixes ### Removed CommonJs Node.js API We've removed the CommonJs Node.js API that we deprecated in 16.0.0. You should update your config to use full paths for locators, such as `extends` and `plugins`: ```diff js export default { - extends: ["./"] - plugins: ["./plugin/"] + extends: ["./index.js"] + plugins: ["./plugin/index.js"] } ``` And when using our [Jest preset](https://www.npmjs.com/package/jest-preset-stylelint) to test your plugin(s): ```diff js import { getTestRule } from 'jest-preset-stylelint'; -global.testRule = getTestRule({ plugins: ['./'] }); +global.testRule = getTestRule({ plugins: ['./index.js'] }); ``` We also recommend updating your config, plugin(s), and any consuming code to ESM (ECMAScript modules) as detailed in our [16.0.0 migration guide](./to-16.md#deprecated-commonjs-api). ### Removed `output` property in the Node.js API returned resolved object We've removed the `output` property that we [deprecated in 16.0.0](./to-16.md#changed-nodejs-api-returned-resolved-object). If you use `stylelint.lint()` and `fix: true`, you should update your code to: ```diff js const result = await stylelint.lint({ code: "a {}", fix: true }); -const fixedCode = result.output; +const formattedProblems = result.report; +const fixedCode = result.code; ``` ### Removed support for Node.js less than 20.19.0 We've removed support for Node.js versions less than 20.19.0, including end-of-life 18.x, to update all our dependencies. We update our dependencies on a [cool-down](https://github.blog/changelog/2025-07-01-dependabot-supports-configuration-of-a-minimum-package-age/) to improve supply chain security, alongside [immutable releases](https://github.blog/changelog/2025-08-26-releases-now-support-immutability-in-public-preview/) on GitHub and [trusted publishing](https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/) to npm. We recommend updating to the latest version of Node.js. ### Removed GitHub formatter We've removed the GitHub formatter than [we deprecated in 16.8.0](../../CHANGELOG.md#1680---2024-07-29). You can use a formatter maintained by the community instead, such as [@csstools/stylelint-formatter-github](https://www.npmjs.com/package/@csstools/stylelint-formatter-github): ```shell npm install --save-dev @csstools/stylelint-formatter-github ``` Using the Node.js API: ```diff js import stylelint from "stylelint"; +import formatter from "@csstools/stylelint-formatter-github"; const result = await stylelint.lint({ files: "**/*.css", - formatter: "github", + formatter, }); ``` Using the CLI: ```diff shell -stylelint "**/*.css" --formatter github +stylelint "**/*.css" --custom-formatter @csstools/stylelint-formatter-github ``` ### Removed `resolveNestedSelectors` option from `selector-class-pattern` We've removed the `resolveNestedSelectors` option from `selector-class-pattern` because it's incompatible with [standard CSS Nesting](https://drafts.csswg.org/css-nesting/), which doesn't concatenate strings. For example, the following is valid SCSS but invalid CSS: ```css .foo { &__bar {} } ``` The SCSS community has created their own plugin version of this rule that reinstates the option using nonstandard nesting with string concatenation. You can use their custom rule instead: ```diff json { "extends": ["stylelint-config-standard-scss"], "rules": { - "selector-class-pattern": [ + "scss/selector-class-pattern": [ "^[a-z]+((-|--|__)[a-z]+)*$", { "resolveNestedSelectors": true } ], } } ``` Alternatively, you can update your config to remove the option and adjust the pattern regex to match unresolved class selectors. For example: ```diff json -{ "selector-class-pattern": ["^[a-z]+((-|--|__)[a-z]+)*$", { "resolveNestedSelectors": true }]} +{ "selector-class-pattern": "^[a-z]"} ``` ### Removed `checkContextFunctionalPseudoClasses` option from `selector-max-id` We removed the `checkContextFunctionalPseudoClasses` option from `selector-max-id` because checking context functional pseudo classes is [now the default behaviour](#changed-selector-max--syntax-rules-for-standard-css-nesting-and-modern-functional-pseudo-classes) for the rule. You should remove the option from your config: ```diff json -{ "selector-max-id": [2, { "checkContextFunctionalPseudoClasses": ["has"] }]} +{ "selector-max-id": 2 } ``` ### Changed default `fix` mode to `strict` We've changed the default mode of `fix` to `strict`: one of the two modes that [we added in 16.13.0](../../CHANGELOG.md#16130---2025-01-12). In `strict` mode, Stylelint will only fix problems when there are no syntax errors in the source. This mode is safer than the `lax` mode, especially when fixing code that uses nesting. If you want to reinstate the previous behaviour, you can set the `lax` value for the `--fix` flag: ```diff shell -stylelint "**/*.css" --fix +stylelint "**/*.css" --fix=lax ``` We've also removed the use of `fix=false` to turn off fixing. Remove the flag entirely instead: ```diff shell -stylelint "**/*.css" --fix=false +stylelint "**/*.css" ``` ### Changed `report` to be consistent and predictable in how it handles the provided position arguments We've changed `report` so that it'll throw an error for the ambiguous position arguments that we deprecated in [16.13.0](https://stylelint.io/CHANGELOG#16130---2025-01-12). You should update your plugin(s) to: - always provide a PostCSS `node` - provide both `start` and `end` as valid positions, if providing a [range](https://postcss.org/api/#range) within the given node - provide both `index` and `endIndex` as numbers, if providing an index range within the given node For example: ```diff js root.walkRules((ruleNode) => { const { selector } = ruleNode; report({ result, ruleName, message: messages.rejected(), node: ruleNode, index, + endIndex: index + selector.length }); }); ``` ### Changed `selector-max-*` syntax rules for standard CSS nesting and modern functional pseudo-classes We've changed the following `selector-max-*` syntax rules to account for CSS nesting and modern functional pseudo-classes, such as `:is()`, `:where()` and `:has()`: - `selector-max-attribute` - `selector-max-class` - `selector-max-combinators` - `selector-max-compound-selectors` - `selector-max-id` - `selector-max-pseudo-class` - `selector-max-type` - `selector-max-universal` So that they no longer: - desugar nesting - evaluate functional pseudo-classes separately, such as `:is()` and `:not()` Standard nesting often desugars to `:is()`. For example, the following: ```css #foo, .bar { & a {} } ``` Is desugared to: ```css :is(#foo, .bar) a {} ``` This behaviour, when hidden, can lead to surprising lint problems being flagged. As we no longer desugar nesting in these rules, the selectors you see in your source files are now linted as-is, so there are no surprises. As these rules no longer evaluate functional pseudo-classes separately, you may need to increase your maxes. For example: ```css .foo:has(.bar) {} ``` ```diff json -{ "selector-max-class": 1 } +{ "selector-max-class": 2 } ``` ### Changed `*-specificity` semantic rules for standard CSS nesting We've changed the following semantic rules to adhere to the [CSS Nesting specification](https://drafts.csswg.org/css-nesting/) when calculating specificity: - `no-descending-specificity` - `selector-max-specificity` The [specificity](https://drafts.csswg.org/selectors-4/#specificity-rules) of the [nesting selector](https://drafts.csswg.org/css-nesting/#nest-selector) (`&`) is equal to the largest specificity among the complex selectors in the parent's selector list (identical to the behavior of [`:is()`](https://drafts.csswg.org/selectors-4/#matches)). For example, given: ```css #foo, .bar { & a {} } /* is equivalent to: */ /* :is(#foo, .bar) a {} */ ``` The specificity of `& a` is `1,0,1` because `#foo` (`1,0,0`) has a larger specificity than `.bar` (`0,1,0`). You may need to adjust your max specificity in your configuration accordingly. For example: ```diff json -{ "selector-max-specificity": "0,1,1" } +{ "selector-max-specificity": "1,1,1" } ``` If you use a language extension that uses nonstandard nesting, such as SCSS or Less, you should turn off the `*-specificity` rules in your config. The SCSS community has turned off the `no-descending-specificity` rule in the latest version of [their shared config](https://www.npmjs.com/package/stylelint-config-standard-scss). ### Changed `no-duplicate-selectors` and `selector-no-qualifying-type` for standard CSS nesting We've changed the following rules to resolve selectors according to [standard nesting](https://drafts.csswg.org/css-nesting), rather than nonstandard nesting of language extensions such as SCSS, before checking them: - `no-duplicate-selectors` - `selector-no-qualifying-type` For some complex selectors, standard nesting will desugar using `:is()`. For example: ```css .foo { a { &[bar] {} } } ``` Is desugared to: ```css [bar]:is(.foo a) {} ``` Where `a` is the qualifying type selector for `[bar]`. If you use a language extension that uses nonstandard nesting, such as SCSS or Less, you should turn off the `no-duplicate-selectors` and `selector-no-qualifying-type` rules in your config. The SCSS community has turned off the `no-duplicate-selectors` rule in the latest version of [their shared config](https://www.npmjs.com/package/stylelint-config-standard-scss). ### Changed `*-list` rules to have consistent behaviour for vendor prefixes and case We've changed the following `*-list` rules so that their treatment of vendor prefixes and case is consistent with the other `*-list` rules and with the `ignore*: []` secondary options: - `at-rule-allowed-list` (added regex support) - `at-rule-disallowed-list` (added regex support) - `declaration-property-unit-allowed-list` - `declaration-property-unit-disallowed-list` - `declaration-property-value-allowed-list` - `declaration-property-value-disallowed-list` - `function-allowed-list` - `function-disallowed-list` - `media-feature-name-value-allowed-list` - `property-allowed-list` - `property-disallowed-list` - `selector-pseudo-class-allowed-list` - `selector-pseudo-class-disallowed-list` - `selector-pseudo-element-allowed-list` - `selector-pseudo-element-disallowed-list` These rules silently allowed or disallowed vendor-prefixed versions and different cases of the specified constructs. For example: ```json { "at-rule-allowed-list": ["supports"] } ``` Would problematically allow nonexistent and different case at-rules such as: `@-webkit-SUPPORTS`. If you want to (dis)allow prefixes before constructs (in either case), you can use a regular expression (and include the `i` modifier). For example: ```diff json -{ "at-rule-allowed-list": ["keyframes"] } +{ "at-rule-allowed-list": ["/^(-webkit-|-moz-)?keyframes$/i"] } ``` ### Changed `*-no-vendor-prefix` rules to have consistent behaviour for their `ignore*: []` secondary options We've changed the following `*-no-vendor-prefix` rules so that their `ignore*: []` secondary options are consistent with the other `*-no-vendor-prefix` rules: - `property-no-vendor-prefix` - `value-no-vendor-prefix` They now check properties and values as-is, respectively, rather then checked prefixed ones. You should update your configuration to include the prefixes properties and values you want to ignore. For example: ```diff json { "property-no-vendor-prefix": [ true, - { "ignoreProperties": ["transform"] } + { "ignoreProperties": ["/^(-webkit-|-moz-)transform$/"] } ] "value-no-vendor-prefix": [ true, - { "ignoreValues": ["grab"] } + { "ignoreValues": ["-webkit-grab"] } ] } ``` ### Changed `declaration-property-max-values` rule to have consistent behaviour for vendor prefixes We've changed the `declaration-property-max-values` rule to check properties as-is, rather than also check prefixed ones. You should update your configuration to include the prefixed properties you want to check. For example: ```diff json { "declaration-property-max-values": { - "transform": 1 + "/(-webkit-)?transform$/": 1 } } ```