Stylelint Configuration: Catch CSS Errors Before They Reach Prod
Set up Stylelint to catch CSS errors, enforce order, and prevent broken styles from shipping. A practical config guide for React and Tailwind projects.
Why CSS Linting Gets Ignored (And Why That's a Mistake)
Honestly, CSS linting is the thing almost every team skips until someone pushes a typo in a border-radius value that breaks the checkout page on mobile. Then suddenly everyone's interested.
JavaScript gets ESLint. TypeScript gets the compiler. CSS gets... a prayer. That's the standard setup on most projects, and it shows — duplicate properties, invalid units, vendor prefixes that don't exist anymore, property values that silently do nothing. None of it throws an error. It just quietly ships.
Stylelint (currently at v16.x) fixes this. It's a linter specifically for CSS, SCSS, and CSS-in-JS, and it can catch actual errors — not just style preferences — before they ever hit your CI pipeline or your users.
Installing Stylelint in a React + Tailwind Project
Getting started takes about five minutes. You'll need the core package plus a config for whatever flavor of CSS you're writing. For most React projects using Tailwind CSS v4.0.2 alongside custom component styles, the standard config is the right starting point.
npm install --save-dev stylelint stylelint-config-standard
# If you're using SCSS:
npm install --save-dev stylelint-config-standard-scss
# For CSS ordering (highly recommended):
npm install --save-dev stylelint-orderAfter installing, create a .stylelintrc.json at your project root. Don't put it inside src/ — Stylelint looks for config files starting from the file being linted and walking up, so root-level is correct. If you already have an ESLint config at root, this lives right next to it.
A Solid Base Configuration That Actually Works
Here's a config that catches real bugs without turning every CSS file into a red underline festival. The goal is signal over noise — flag things that are genuinely wrong, not things that are just stylistically different from someone's preferences.
{
"extends": ["stylelint-config-standard"],
"plugins": ["stylelint-order"],
"rules": {
"color-no-invalid-hex": true,
"unit-no-unknown": true,
"property-no-unknown": true,
"declaration-block-no-duplicate-properties": true,
"shorthand-property-no-redundant-values": true,
"alpha-value-notation": "number",
"color-function-notation": "modern",
"order/properties-order": [
"position",
"top", "right", "bottom", "left",
"display",
"flex-direction", "align-items", "justify-content",
"gap",
"width", "height",
"margin", "padding",
"background",
"border", "border-radius",
"color",
"font-size", "font-weight",
"transition",
"z-index"
]
},
"ignoreFiles": ["node_modules/**", "dist/**", ".next/**"]
}The order/properties-order rule is optional but worth it. Consistent property ordering means code reviews stop being about 'why is z-index before display' and start being about what actually matters. It also makes diffs cleaner — properties always appear in the same position, so you're not hunting through a block to find what changed.
Handling Tailwind Utility Classes Without False Positives
The biggest friction point with Stylelint in Tailwind projects isn't the CSS files — it's the inline class scanning and the CSS that Tailwind itself generates. By default, Stylelint will complain about @tailwind base, @apply, and custom @layer blocks because they're not standard CSS. You need to tell it to back off.
{
"extends": ["stylelint-config-standard"],
"rules": {
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"layer",
"config",
"plugin",
"source",
"utility"
]
}
],
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme", "screen"]
}
]
}
}The theme() and screen() functions are Tailwind-specific and not valid CSS — without the ignore list, Stylelint will flag every usage. Similarly, @apply is how you pull utility classes into custom components, and @layer is standard in Tailwind v4 for style organization. Ignoring these isn't giving up on linting, it's just telling the tool what your actual stack looks like.
If you're using Empire UI components and writing custom CSS on top — for instance, adding a backdrop-filter: blur(12px) and background: rgba(255,255,255,0.15) for a glassmorphism effect — these rules will still catch typos and invalid values in your hand-written styles.
Adding Stylelint to Your npm Scripts and CI
Linting that doesn't run automatically doesn't run. Add it to package.json so it's part of your normal workflow. Combine it with your existing lint script or run it separately — both approaches work fine.
{
"scripts": {
"lint": "eslint . && stylelint '**/*.css'",
"lint:css": "stylelint '**/*.css' --fix",
"lint:scss": "stylelint '**/*.scss' --fix"
}
}The --fix flag handles auto-fixable issues — property ordering, color notation, shorthand redundancy. It won't fix invalid values because there's no way to guess what you meant. For CI, drop the --fix flag and let the pipeline fail on errors. That's the whole point.
For GitHub Actions, add a step after your existing lint check: run: npx stylelint '**/*.css' --max-warnings 0. The --max-warnings 0 flag makes warnings fail the build too. You can relax this once you've cleaned up existing issues, but starting strict is easier than adding strictness later.
CSS Custom Properties and Variable Errors Stylelint Catches
One category of errors that's surprisingly hard to spot in code review: using CSS custom properties that don't exist. You reference var(--color-priary) instead of var(--color-primary) and nothing breaks at build time. The browser just silently uses the fallback or shows nothing. Stylelint can't catch undefined variables natively, but the stylelint-value-no-unknown plugin and strict project conventions get you most of the way there.
What Stylelint does catch out of the box: duplicate custom property declarations in the same block, invalid values passed to properties (like gap: 8; instead of gap: 8px;), and using deprecated syntax. That gap: 8; example is real — unitless values are valid for things like flex-grow, but not for gap. It's the kind of bug that renders fine in Chrome but breaks in Safari.
If you're generating box shadows or other complex values with tools, pipe the output through Stylelint before committing. It's much easier to validate box-shadow: 0 4px 24px rgba(0,0,0,0.18) at generation time than to debug layout issues on a client call.
Integrating Stylelint with VS Code and Your Editor
Running linting from the CLI is fine for CI, but you want inline feedback while writing code. The Stylelint VS Code extension (extension ID: stylelint.vscode-stylelint) shows errors in the editor as you type, with red underlines and hover descriptions of what's wrong.
Add this to your .vscode/settings.json to enable it for the file types you're using:
{
"stylelint.validate": ["css", "scss", "postcss"],
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
},
"css.validate": false,
"scss.validate": false
}The css.validate: false line is important. VS Code has its own built-in CSS validation, and it'll conflict with Stylelint — you'll get duplicate warnings for the same line, from different sources, with different messages. Disabling the built-in validator and letting Stylelint handle everything is cleaner. If you're also using the Tailwind CSS IntelliSense extension, it plays fine alongside Stylelint — they cover different things.
When Stylelint Isn't Enough (And What to Pair It With)
Stylelint catches syntax and property errors. It doesn't catch visual bugs. A valid border-radius: 50% on a rectangle looks fine to the linter and terrible in production. That's a different problem — one that requires visual regression testing or at least a good design review process.
For teams building component libraries, pairing Stylelint with a tool like Storybook gives you both: Stylelint catches the CSS mistakes, Storybook lets you visually verify the output. If you're using Empire UI's gradient generator or shadow tools to produce values, run those generated styles through your Stylelint check before embedding them in components.
Does Stylelint slow down your dev loop? Not meaningfully. The VS Code extension runs checks asynchronously. The CLI on a medium project runs in under two seconds. The cost-benefit here isn't even close — catching a missing unit on a padding value in code review takes thirty seconds. Debugging why a layout broke in production takes an hour. Set up Stylelint once and stop thinking about it.
FAQ
Yes. Point it at your .module.css files with a glob like '**/*.module.css' in your lint script. The rules apply the same way — CSS Modules is just CSS with scoped class names, so there's nothing special to configure. You may want to add composes to the at-rule-no-unknown ignore list since it's a CSS Modules-specific keyword.
Add @apply to the ignoreAtRules array in your at-rule-no-unknown rule config. The full list to ignore for a typical Tailwind v4 project is: tailwind, apply, layer, config, plugin, source, utility. Without these, Stylelint will report an error on every Tailwind directive in your CSS.
Stylelint can auto-fix a subset of issues — property ordering, color notation (like converting rgb(255 255 255 / 0.5) to the modern syntax), and redundant shorthand values. Run stylelint '**/*.css' --fix to apply fixes in place. It won't fix invalid values or unknown properties because it can't guess the intended correct value.
stylelint-config-recommended enables only the rules that catch actual errors — invalid syntax, unknown properties, etc. stylelint-config-standard extends it and adds stylistic rules like consistent notation, no empty blocks, and single quotes for strings. Start with standard and disable specific rules that conflict with your project's existing patterns.
Use the ignoreFiles array in your .stylelintrc.json. Common entries: node_modules/**, dist/**, .next/**, build/**. You can also create a .stylelintignore file at the project root — it works exactly like .gitignore, one pattern per line. The ignoreFiles approach in the config is simpler for most projects.
It can, but there's a fix. Install stylelint-config-prettier and add it as the last item in your extends array — this disables all Stylelint rules that would conflict with Prettier's formatting decisions. The rule of thumb: let Prettier handle formatting (whitespace, quotes, line breaks), let Stylelint handle errors and property validity.