Skip to main content

Migrating to 18.0.0

This release contains significant or breaking changes.

We've made our rules check values more accurately, including those that use custom properties, and replaced dependencies with Node.js built-ins and smaller packages to improve supply chain security.

Significant changes​

We've:

  • fixed declaration-property-value-no-unknown false negatives for values containing custom properties
  • added declaration-property-custom-property-allowed-list and value-type-custom-property-required-list rules
  • added value-no-invalid rule
  • added result.stylelint.postcss property for plugins

Fixed declaration-property-value-no-unknown false negatives for values containing custom properties​

We've fixed declaration-property-value-no-unknown false negatives for values containing custom properties by substituting their declared values for var() functions before checking the declaration value.

For example, var(--foo) is now flagged, as red is unknown to the top property:

:root {
--foo: red;
}

a {
top: var(--foo);
}

For custom properties registered with @property, the rule substitutes only the declared values that match the registered syntax, or a value of the syntax when none do. Given:

@property --foo {
syntax: "<color>";
}

a {
--foo: 1px;
top: var(--foo);
}
  • 1px is flagged, as --foo is registered as <color>
  • var(--foo) is now flagged, as <color> values are unknown to the top property

The problem message for the former now says what the custom property is registered as, e.g. Unknown value "1px" for property "--foo", registered as "<color>", and passes the syntax as a fourth message argument.

You can use the referenceFiles configuration property to include custom properties declared in separate files:

{
- "extends": ["stylelint-config-standard"]
+ "extends": ["stylelint-config-standard"],
+ "referenceFiles": "tokens/*.css"
}

Added declaration-property-custom-property-allowed-list and value-type-custom-property-required-list rules​

We've added the declaration-property-custom-property-allowed-list rule to specify which custom properties are allowed in which properties, and the experimental value-type-custom-property-required-list rule to specify which value types require custom properties.

For example, to allow only --spacing-* custom properties in margin and padding properties and only --radii-* in border-radius, and to require custom properties for <length> values:

{
"extends": ["stylelint-config-standard"],
"rules": {
+ "declaration-property-custom-property-allowed-list": {
+ "/^(margin|padding)/": ["/^--spacing-/"],
+ "border-radius": ["/^--radii-/"]
+ },
+ "value-type-custom-property-required-list": ["length"]
}
}

Given:

a {
border-radius: var(--spacing-m);
margin: 1rem;
}

The customizing guide has a fuller example that uses a tokens file.

Added value-no-invalid rule​

We've added the value-no-invalid rule. It flags values that cannot be parsed and math expressions that add or subtract a number to a dimension or percentage, for example:

a {
top: calc(1px + 2);
}

The declaration-property-value-no-unknown rule previously made these checks. See the breaking change below.

We've turned the rule on in the latest version of the standard config, along with four other rules that help you avoid errors:

Added result.stylelint.postcss property for plugins​

You can now access the version of PostCSS that Stylelint uses from the result object in your plugin(s).

You should update your plugin(s) to use it rather than import postcss, as PostCSS rebuilds nodes that cross different versions:

-import postcss from "postcss";
-
const ruleFunction = (primary) => {
return (root, result) => {
- const styleRule = postcss.rule({ selector: ".foo" });
+ const styleRule = result.stylelint.postcss.rule({ selector: ".foo" });
};
};

Breaking changes​

We've removed:

  • support for end-of-life and older versions of Node.js
  • context.fix property
  • support for CommonJS-style filename-less and extension-less locator paths
  • declaration-block-single-line-max-declarations rule
  • *syntax options from declaration-property-value-no-unknown
  • ignoreSelectors option from selector-max-specificity
  • resolution of packages from the global node_modules directory

And changed:

  • the CLI to parse arguments with node:util.parseArgs
  • globbyOptions to support only a subset of options
  • declaration-property-value-no-unknown to no longer report invalid values
  • TypeScript config files to be loaded by Node.js's built-in type stripping
  • color detection to use new color option with node:util.styleText
  • hyperlink detection

Removed support for end-of-life and older versions of Node.js​

We've removed support for Node.js versions less than 22.22.2, 24.15.0 and 26.0.0, including end-of-life 20.x, 23.x and 25.x, to update all our dependencies.

We recommend updating to the latest version of Node.js.

Removed context.fix property​

We've removed the context.fix property that we deprecated in 16.8.2.

You should update your plugin(s) to pass a fix callback to the report utility instead, which handles configuration comments for you:

root.walkRules((ruleNode) => {
const { selector } = ruleNode;

if (!selector.includes("foo")) return;

- if (context.fix) {
- ruleNode.selector = selector.replace("foo", "bar");
- return;
- }
-
report({
result,
ruleName,
message: messages.rejected(selector),
node: ruleNode,
+ fix: () => {
+ ruleNode.selector = selector.replace("foo", "bar");
+ },
});
});

And set meta.fixable to true on the rule:

const meta = {
url: "https://example.com/my-rule",
+ fixable: true,
};

Removed support for CommonJS-style filename-less and extension-less locator paths​

We've removed support for CommonJS-style filename-less and extension-less paths in locators, such as extends and plugins, to align with ESM import behaviour.

You should update your config to use full paths:

export default {
- extends: ["./", "stylelint-config-foo/bar"],
- plugins: ["./plugin"],
+ extends: ["./index.js", "stylelint-config-foo/bar.js"],
+ plugins: ["./plugin.js"],
};

And when using our Jest preset to test your plugin(s):

-global.testRule = getTestRule({ plugins: ["./"] });
+global.testRule = getTestRule({ plugins: ["./index.js"] });

Removed declaration-block-single-line-max-declarations rule​

We've removed the declaration-block-single-line-max-declarations rule, which we overlooked when we removed the stylistic rules in 16.0.0.

You should remove the rule from your configuration object, or use the @stylistic/declaration-block-single-line-max-declarations rule of the community @stylistic/stylelint-plugin instead:

{
+ "plugins": ["@stylistic/stylelint-plugin"],
"rules": {
- "declaration-block-single-line-max-declarations": 1
+ "@stylistic/declaration-block-single-line-max-declarations": 1
}
}

Removed *syntax options from declaration-property-value-no-unknown​

We've removed the propertiesSyntax and typesSyntax options from declaration-property-value-no-unknown that we deprecated in 17.5.0.

You should update your config to use the shared and more performant languageOptions configuration property instead:

{
+ "languageOptions": {
+ "syntax": {
+ "properties": { "top": "| <--foo()>" },
+ "types": { "--foo()": "--foo( <length-percentage> )" }
+ }
+ },
"rules": {
- "declaration-property-value-no-unknown": [
- true,
- {
- "propertiesSyntax": { "top": "| <--foo()>" },
- "typesSyntax": { "--foo()": "--foo( <length-percentage> )" }
- }
- ]
+ "declaration-property-value-no-unknown": true
}
}

Removed ignoreSelectors option from selector-max-specificity​

We've removed the ignoreSelectors option from selector-max-specificity. We originally added the option to ignore the :global() and :local() pseudo-classes of CSS Modules. The rule now gives these no specificity of their own.

You should remove the option from your configuration object:

{
"rules": {
- "selector-max-specificity": ["0,2,0", { "ignoreSelectors": [":global", ":local"] }]
+ "selector-max-specificity": "0,2,0"
}
}

If you used the option for cases other than :global() and :local(), you may need to increase the maximum or reconsider the selector structure, since the rule no longer supports per-selector exceptions.

Removed resolution of packages from the global node_modules directory​

We've removed the fallback that resolved extends and plugins packages from npm's global node_modules directory, to rely on Node.js's module resolution instead.

You should install the packages your config uses locally, as the Node.js documentation strongly encourages:

npm install --save-dev stylelint stylelint-config-standard

If you share one config across projects, two workarounds remain, though we don't recommend either:

  • keep the config file and its node_modules in an ancestor directory, such as your home directory, as Stylelint currently searches upwards for a config file
  • resolve the packages from npm's global root in your config file:
// stylelint.config.mjs
import { execSync } from "node:child_process";
import path from "node:path";

const globalRoot = execSync("npm root -g", { encoding: "utf8" }).trim();

export default {
extends: [path.join(globalRoot, "stylelint-config-standard")]
};

Changed the CLI to parse arguments with node:util.parseArgs​

We've changed the CLI to parse arguments with node:util.parseArgs, which accepts fewer spellings of flags.

If you separate the value of --fix with a space, you should attach it with = instead, as the CLI now reads the value as a file path:

-stylelint "**/*.css" --fix lax
+stylelint "**/*.css" --fix=lax

The CLI reports the following changes as invalid options and names the flag to use instead.

If you use an alias of a long flag, you should use the flag itself:

-stylelint "**/*.css" --aei --mw 0
+stylelint "**/*.css" --allow-empty-input --max-warnings 0
AliasFlag
--aei--allow-empty-input
--cei--compute-edit-info
--di--disable-default-ignores
--go--globby-options
--id--ignore-disables
--ip--ignore-pattern
--mw--max-warnings
--rd--report-needless-disables
--rdd--report-descriptionless-disables
--risd--report-invalid-scope-disables
--rud--report-unscoped-disables

If you use camelCase flags, you should update them to their kebab-case names:

-stylelint "**/*.css" --ignorePath .gitignore --allowEmptyInput
+stylelint "**/*.css" --ignore-path .gitignore --allow-empty-input

If you give a boolean flag a value with =, you should use the flag or its --no- form:

-stylelint "**/*.css" --cache=false
+stylelint "**/*.css" --no-cache

If you attach the value of a short flag with =, you should separate it with a space:

-stylelint "**/*.css" -c=stylelint.config.mjs
+stylelint "**/*.css" -c stylelint.config.mjs

Changed globbyOptions to support only a subset of options​

We've replaced globby with tinyglobby to reduce our transitive dependencies. As a result, the globbyOptions option and its --globby-options flag now support only the options that behave the same with tinyglobby:

  • braceExpansion
  • caseSensitiveMatch
  • dot
  • extglob
  • followSymbolicLinks
  • ignore

If you used other options, you should replace them:

  • cwd with the top-level cwd option, which relative globs are now resolved from
  • gitignore with the ignorePath option (--ignore-path on the CLI) pointing at your .gitignore file

For example:

-stylelint "**/*.css" --globby-options '{"gitignore":true}'
+stylelint "**/*.css" --ignore-path .gitignore

Unlike globby's gitignore option, the ignorePath option:

  • reads only the files you name, not .gitignore files in subdirectories or parent directories
  • resolves patterns relative to cwd, not to the directory of the .gitignore file
  • replaces the default .stylelintignore lookup, so you should also name your .stylelintignore file if you use one, e.g. --ignore-path .gitignore --ignore-path .stylelintignore

Other options, such as deep and expandDirectories, no longer have an equivalent.

We may migrate to the Node.js built-in glob API, such as node:fs.glob and node:path.matchesGlob, in the future. We recommend using globs that are compatible with it: name dot directories, e.g. ".storybook/**/*.css", instead of using the dot option, and wrap special characters in square brackets, e.g. "[[]foo[]]/**/*.css", instead of escaping them with backslashes.

Changed declaration-property-value-no-unknown to no longer report invalid values​

We've moved the checks for values that cannot be parsed and for invalid math expressions from declaration-property-value-no-unknown to the new value-no-invalid rule, as neither depends on the property.

If you don't extend the standard config, you should turn on the new rule to keep reporting these problems:

{
"rules": {
- "declaration-property-value-no-unknown": true
+ "declaration-property-value-no-unknown": true,
+ "value-no-invalid": true
}
}

Changed TypeScript config files to be loaded by Node.js's built-in type stripping​

We've changed TypeScript config files, such as stylelint.config.ts, to be loaded by Node.js's type stripping rather than the typescript package, which you no longer need to install.

Only erasable syntax is supported. You should update your config to:

  • use import type for type-only imports
  • replace syntax that needs transforming, such as enum and namespace

For example:

-import { Config } from "stylelint";
+import type { Config } from "stylelint";

Changed color detection to use new color option with node:util.styleText​

We've changed our built-in formatters to use the new color option to force color on or off, rather than looking for --color and --no-color in the process arguments.

The --color and --no-color CLI flags are unchanged.

If you use the Node.js API with the string or verbose formatter and relied on --color or --no-color in your process arguments, you should pass the color option instead:

const { report } = await stylelint.lint({
files: "**/*.css",
formatter: "string",
+ color: false,
});

We've also replaced picocolors with node:util.styleText to reduce our dependencies. When the color option isn't set, FORCE_COLOR takes precedence over NO_COLOR, and colors are off when the NODE_DISABLE_COLORS environment variable is set or FORCE_COLOR is 0.

If you want color in your CI logs, you should use the --color flag or set the FORCE_COLOR environment variable:

-stylelint "**/*.css"
+stylelint "**/*.css" --color

The color option and its flags take precedence over all these environment variables.

We've changed hyperlink detection to no longer depend on color, the FORCE_HYPERLINK environment variable or a list of terminals known to support hyperlinks.

The string and verbose formatters now add hyperlinks in any terminal except in CI or on Windows outside Windows Terminal.