At Testim.io we care about code quality and UX. For this reason, we use various tools that make development easier and more accurate. Among others, we use Stylelint to lint our SCSS and CSS files. One of the powerful features of stylelint is the ability to add rules of your own, which will fit your project’s needs. While stylelint provides an API for adding custom rules, it can be a bit confusing when doing so for the first time.
In this blog post, we will go through the steps for adding a new custom stylelint rule to your project:
- Declaring and creating the plugin
- Adding a rule to the plugin
- Implementing the linting function along with auto-fix support
- Integrating the plugin with your project’s Stylelint configuration
Background story
In our application’s codebase, we store commonly used colors in variables. This makes color theme changes very quick and easy. While developing a visual component, we use Invision’s mockups as a reference. Invision isn’t aware of the color variables we use. Thus, Copying styles from Invision involves searching for the colors’ variables (e.g. replace #ff6363 with $color-red1).
This seems like an easy thing to do: use “find anywhere” and look for this color. Yet, the process becomes cumbersome — especially if we include many colors. Additionally, in some cases of legacy code, the variable ($color-red1) was not used. Hence, we can find the same color (#ff6363) scattered in the codebase. In such cases, the developer has to find the correct declaration in all search results.
In order to solve this inconvenience, we wrote a custom stylelint rule. It suggests the correct variable name, and even auto-fixes the errors. How convenient!

Writing the Plugin — First Steps
Let’s start with creating a file that will contain the rule itself — in our case, it’s no-color-blue.js.
Next, you’ll need to define your rule. This way stylelint will know what to run when the rule appears in the configurations:
// no-color-blue.js const stylelint = require('stylelint'); const { ruleMessages, validateOptions } = stylelint.utils; const ruleName = 'testim-plugin/no-color-blue'; const messages = ruleMessages(ruleName, { //… linting messages can be specified here });
Your file will need to export three things: the rule name, the messages that it shows, and the rule itself.
module.exports.ruleName = ruleName; module.exports.messages = messages; module.exports = stylelint.createPlugin(ruleName, function ruleFunction(primaryOption, secondaryOptionObject, context) { return function lint(postcssRoot, postcssResult) { // ... }});
If it still doesn’t make much sense to you, that’s ok! In the next sections, we’ll explain each part.
Creating a Plugin
We use stylelint.createPlugin to create and register a stylelint rule. It receives two arguments: the rule name, and the ruleFunction itself.
stylelint calls ruleFunction if the rule is used in the linting process. The function receives the configured options, along with the linting context object.
The lint function (that ruleFunction returns) will run on each file that is being linted. This is the actual core of the rule which will report the output for the linting process.
Let’s take a deeper look at ruleFunction and lint.
Adding a Rule to the Plugin
As mentioned before, stylelint calls ruleFunction for each single linting “process”. Stylelint’s rule support options. These can be used to customize the rule’s behavior from the stylelint’s configuration. For example, most rules support the severity option. When using your rule, the ruleFunction will be called with the matching options (primary and secondary).
Furthermore, stylelint provides a handy validateOptions function. Use it to automatically report errors if the user passed invalid options. Stylelint will add the errors to the linting result (called here postcssResult). The documentation isn’t too extensive, but the source code has some good comments.
The third argument of ruleFunction is the context object. It is mainly used for supporting auto-fix (more on that later).
The following snippet should make things clearer:
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) { return function lint(postcssRoot, postcssResult) { const validOptions = validateOptions( postcssResult, ruleName, { //Options schema goes here } ); if (!validOptions) { //If the options are invalid, don't lint return; } const shouldDoExtraCoolStuff = secondaryOptionObject.doStuff; //Parse the options to customize behaviour const isAutoFixing = Boolean(context.fix); //context.fix will be "true" if auto-fix mode is active
We’re done with setting up the rule. Now, let’s write the lint function which holds the core logic of the rule.
Performing the Linting
After creating your rule, stylelint will run PostCSS on each linted file. Next, in the linting phase, stylelint will call the custom rule’s lint function. The lint function is at the core of your rule. It receives two arguments:
- postcssRoot — the parsed AST. It “represents a CSS file and contains all its parsed nodes”.
- postcssResult — the LazyResult which will accumulate the output of your rule’s linting.
Usually, the lint function will:
- “Walk” through the AST (the parsed CSS nodes). Use one of the root.walk methods for this task.
- Detect invalid nodes.
- Report these nodes (or fix them, depending on the “fix” flag).
Let’s examine the complete example of our rule. Reminder — the rule forbids the color name blue and expects it to be replaced with the hex RGB color #0000FF.
//no-blue-color.js const stylelint = require('stylelint'); const { report, ruleMessages, validateOptions } = stylelint.utils; const ruleName = 'testim-plugin/no-blue-color'; const messages = ruleMessages(ruleName, { expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`, }); module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) { return function lint(postcssRoot, postcssResult) { const validOptions = validateOptions( postcssResult, ruleName, { //No options for now... } ); if (!validOptions) { //If the options are invalid, don't lint return; } const isAutoFixing = Boolean(context.fix); postcssRoot.walkDecls(decl => { //Iterate CSS declarations const hasBlue = decl.value.includes('blue'); if (!hasBlue) { return; //Nothing to do with this node - continue } if (isAutoFixing) { //We are in “fix” mode const newValue = decl.value.replace('blue', '#0000FF'); //Apply the fix. It's not pretty, but that's the way to do it if (decl.raws.value) { decl.raws.value.raw = newValue; } else { decl.value = newValue; } } else { //We are in “report only” mode report({ ruleName, result: postcssResult, message: messages.expected('blue', '#0000FF'), // Build the reported message node: decl, // Specify the reported node word: 'blue', // Which exact word caused the error? This positions the error properly }); } }); }; }); module.exports.ruleName = ruleName; module.exports.messages = messages;
That’s it! All that’s left is adding the rule to stylelint’s config.
Custom Rule Integration
Edit your project’s stylelint configuration (e.g. .stylelintrc.json). Under the “plugins” configuration value, add a path to your plugin’s file. All that’s left is activating your rule, so stylelint will actually run it. Do this by adding your rule to the “rules” configuration value.
A minimal .stylelintrc file will look like so:
//.stylelintrc.json { "plugins": [ "./no-blue-color.js" ], "rules": { "testim-plugin/no-blue-color": true } }
Please note that for enabling a rule, stylelint requires passing options to it. Even if the rule has no options (like our no-color-blue example), it is still required to pass a true option to it. Without it, stylelint will ignore the rule.
That’s it! You and your team can now enjoy your custom linting errors.
Summary
Stylelint is a very useful linting tool. It has many advantages when collaborating on common SCSS and CSS files. It comes with a vast variety of existing rules. Yet, they can’t always answer all your needs. At some point, you’ll need a linting rule that is yet to exist. It might be the project’s structure, your team’s conventions, or anything else. In these cases, custom rules will be very helpful.
I hope that after reading this post, you will be able to quickly create and integrate your own stylelint rules. This way, you will keep a higher level of code quality while respecting your project’s requirements.