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-unknownfalse negatives for values containing custom properties - added
declaration-property-custom-property-allowed-listandvalue-type-custom-property-required-listrules - added
value-no-invalidrule - added
result.stylelint.postcssproperty 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);
}
1pxis flagged, as--foois registered as<color>var(--foo)is now flagged, as<color>values are unknown to thetopproperty
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;
}
--spacing-mis flagged bydeclaration-property-custom-property-allowed-list, as only--radii-*custom properties are allowed inborder-radius1remis flagged byvalue-type-custom-property-required-list, as custom properties are required for<length>values
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.fixproperty- support for CommonJS-style filename-less and extension-less locator paths
declaration-block-single-line-max-declarationsrule*syntaxoptions fromdeclaration-property-value-no-unknownignoreSelectorsoption fromselector-max-specificity- resolution of packages from the global
node_modulesdirectory
And changed:
- the CLI to parse arguments with
node:util.parseArgs globbyOptionsto support only a subset of optionsdeclaration-property-value-no-unknownto no longer report invalid values- TypeScript config files to be loaded by Node.js's built-in type stripping
- color detection to use new
coloroption withnode: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_modulesin 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
| Alias | Flag |
|---|---|
--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:
braceExpansioncaseSensitiveMatchdotextglobfollowSymbolicLinksignore
If you used other options, you should replace them:
cwdwith the top-levelcwdoption, which relative globs are now resolved fromgitignorewith theignorePathoption (--ignore-pathon the CLI) pointing at your.gitignorefile
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
.gitignorefiles in subdirectories or parent directories - resolves patterns relative to
cwd, not to the directory of the.gitignorefile - replaces the default
.stylelintignorelookup, so you should also name your.stylelintignorefile 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 typefor type-only imports - replace syntax that needs transforming, such as
enumandnamespace
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.
Changed hyperlink detection
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.