Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Lint CSS and SCSS files

All CSS/SCSS files are linted with Stylelint to detect errors and enforce standards.

Utilities
Quick take
Typical fix time 20 min
  • Install Stylelint with stylelint-config-standard
  • Add SCSS support with stylelint-config-standard-scss
  • Enable auto-fix in your IDE and pre-commit hooks
  • Include in CI/CD pipeline to catch issues early
Why it matters: CSS linting catches syntax errors, enforces consistent coding standards, and prevents maintainability issues before they reach production.

Rule Details

CSS and SCSS linting tools like Stylelint help detect syntax errors, enforce coding standards, and maintain consistent code quality across stylesheets.

Code Examples

Installation

# Install Stylelint and standard config
npm install --save-dev stylelint stylelint-config-standard
 
# For SCSS support
npm install --save-dev stylelint-config-standard-scss
 
# For CSS-in-JS support
npm install --save-dev stylelint-config-styled-components
 
# Additional useful plugins
npm install --save-dev stylelint-order stylelint-scss

Basic Configuration (.stylelintrc.json)

{
  "extends": ["stylelint-config-standard"],
  "rules": {
    "indentation": 2,
    "string-quotes": "double",
    "no-duplicate-selectors": true,
    "color-hex-case": "lower",
    "color-hex-length": "short",
    "color-named": "never",
    "selector-combinator-space-after": "always",
    "selector-attribute-quotes": "always",
    "selector-attribute-operator-space-before": "never",
    "selector-attribute-operator-space-after": "never",
    "selector-attribute-brackets-space-inside": "never",
    "declaration-block-trailing-semicolon": "always",
    "declaration-colon-space-before": "never",
    "declaration-colon-space-after": "always",
    "declaration-block-single-line-max-declarations": 1,
    "property-no-unknown": true,
    "unit-allowed-list": ["px", "rem", "em", "%", "vh", "vw", "deg", "s", "ms"],
    "shorthand-property-no-redundant-values": true,
    "value-no-vendor-prefix": true,
    "property-no-vendor-prefix": true,
    "comment-pattern": "^.+$",
    "rule-empty-line-before": ["always", {
      "except": ["first-nested"],
      "ignore": ["after-comment"]
    }],
    "at-rule-no-unknown": true,
    "no-descending-specificity": true,
    "max-nesting-depth": 3,
    "selector-max-id": 0,
    "selector-no-qualifying-type": true,
    "font-family-no-missing-generic-family-keyword": true
  },
  "ignoreFiles": [
    "node_modules/**/*",
    "dist/**/*",
    "build/**/*",
    "**/*.min.css"
  ]
}

SCSS Configuration

{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-standard-scss"
  ],
  "plugins": [
    "stylelint-scss",
    "stylelint-order"
  ],
  "rules": {
    "scss/dollar-variable-pattern": "^[a-z][a-zA-Z0-9]*$",
    "scss/at-function-pattern": "^[a-z][a-zA-Z0-9]*$",
    "scss/at-mixin-pattern": "^[a-z][a-zA-Z0-9]*$",
    "scss/percent-placeholder-pattern": "^[a-z][a-zA-Z0-9]*$",
    "scss/at-import-partial-extension-blacklist": ["scss"],
    "scss/at-import-no-partial-leading-underscore": true,
    "scss/at-else-closing-brace-newline-after": "always-last-in-chain",
    "scss/at-else-closing-brace-space-after": "always-intermediate",
    "scss/at-else-empty-line-before": "never",
    "scss/at-if-closing-brace-newline-after": "always-last-in-chain",
    "scss/at-if-closing-brace-space-after": "always-intermediate",
    "scss/dollar-variable-colon-space-after": "always",
    "scss/dollar-variable-colon-space-before": "never",
    "scss/dollar-variable-no-missing-interpolation": true,
    "scss/double-slash-comment-whitespace-inside": "always",
    "scss/operator-no-unspaced": true,
    "scss/selector-no-redundant-nesting-selector": true,
    "order/properties-alphabetical-order": true
  }
}

Advanced Configuration with Property Order

{
  "extends": ["stylelint-config-standard-scss"],
  "plugins": ["stylelint-order"],
  "rules": {
    "order/properties-order": [
      [
        {
          "groupName": "Special",
          "emptyLineBefore": "always",
          "properties": [
            "composes",
            "@import",
            "@extend"
          ]
        },
        {
          "groupName": "Positioning",
          "emptyLineBefore": "always",
          "properties": [
            "position",
            "top",
            "right",
            "bottom",
            "left",
            "z-index"
          ]
        },
        {
          "groupName": "Box Model",
          "emptyLineBefore": "always",
          "properties": [
            "display",
            "flex",
            "flex-grow",
            "flex-shrink",
            "flex-basis",
            "flex-direction",
            "flex-flow",
            "flex-wrap",
            "grid",
            "grid-area",
            "grid-template",
            "grid-template-areas",
            "grid-template-rows",
            "grid-template-columns",
            "grid-row",
            "grid-row-start",
            "grid-row-end",
            "grid-column",
            "grid-column-start",
            "grid-column-end",
            "grid-auto-rows",
            "grid-auto-columns",
            "grid-auto-flow",
            "grid-gap",
            "grid-row-gap",
            "grid-column-gap",
            "gap",
            "row-gap",
            "column-gap",
            "align-content",
            "align-items",
            "align-self",
            "justify-content",
            "justify-items",
            "justify-self",
            "order",
            "float",
            "clear",
            "object-fit",
            "overflow",
            "overflow-x",
            "overflow-y",
            "clip",
            "zoom",
            "width",
            "min-width",
            "max-width",
            "height",
            "min-height",
            "max-height",
            "margin",
            "margin-top",
            "margin-right",
            "margin-bottom",
            "margin-left",
            "padding",
            "padding-top",
            "padding-right",
            "padding-bottom",
            "padding-left"
          ]
        },
        {
          "groupName": "Typography",
          "emptyLineBefore": "always",
          "properties": [
            "color",
            "font",
            "font-family",
            "font-size",
            "font-style",
            "font-variant",
            "font-weight",
            "font-stretch",
            "font-display",
            "line-height",
            "letter-spacing",
            "text-align",
            "text-decoration",
            "text-indent",
            "text-overflow",
            "text-rendering",
            "text-shadow",
            "text-transform",
            "text-wrap",
            "white-space",
            "word-spacing",
            "word-wrap",
            "word-break",
            "tab-size",
            "hyphens"
          ]
        },
        {
          "groupName": "Visual",
          "emptyLineBefore": "always",
          "properties": [
            "background",
            "background-color",
            "background-image",
            "background-position",
            "background-size",
            "background-repeat",
            "background-origin",
            "background-clip",
            "background-attachment",
            "background-blend-mode",
            "border",
            "border-top",
            "border-right",
            "border-bottom",
            "border-left",
            "border-width",
            "border-top-width",
            "border-right-width",
            "border-bottom-width",
            "border-left-width",
            "border-style",
            "border-top-style",
            "border-right-style",
            "border-bottom-style",
            "border-left-style",
            "border-color",
            "border-top-color",
            "border-right-color",
            "border-bottom-color",
            "border-left-color",
            "border-radius",
            "border-top-left-radius",
            "border-top-right-radius",
            "border-bottom-right-radius",
            "border-bottom-left-radius",
            "border-image",
            "border-image-source",
            "border-image-slice",
            "border-image-width",
            "border-image-outset",
            "border-image-repeat",
            "outline",
            "outline-width",
            "outline-style",
            "outline-color",
            "outline-offset",
            "box-shadow",
            "opacity",
            "filter",
            "backdrop-filter"
          ]
        },
        {
          "groupName": "Animation",
          "emptyLineBefore": "always",
          "properties": [
            "transform",
            "transform-origin",
            "transform-style",
            "perspective",
            "perspective-origin",
            "backface-visibility",
            "transition",
            "transition-property",
            "transition-duration",
            "transition-timing-function",
            "transition-delay",
            "animation",
            "animation-name",
            "animation-duration",
            "animation-play-state",
            "animation-timing-function",
            "animation-delay",
            "animation-iteration-count",
            "animation-direction",
            "animation-fill-mode"
          ]
        },
        {
          "groupName": "Misc",
          "emptyLineBefore": "always",
          "properties": [
            "appearance",
            "content",
            "clip-path",
            "counter-reset",
            "counter-increment",
            "resize",
            "cursor",
            "pointer-events",
            "will-change",
            "user-select",
            "list-style",
            "list-style-type",
            "list-style-position",
            "list-style-image",
            "caption-side",
            "table-layout",
            "border-collapse",
            "border-spacing",
            "empty-cells",
            "quotes"
          ]
        }
      ],
      {
        "unspecified": "bottom"
      }
    ]
  }
}

Why It Matters

CSS linting catches syntax errors, enforces consistent coding standards, and prevents maintainability issues before they reach production.

Framework-Specific Linting

React with CSS Modules

{
  "extends": ["stylelint-config-standard"],
  "rules": {
    "selector-pseudo-class-no-unknown": [
      true,
      {
        "ignorePseudoClasses": ["export", "import", "global", "local", "external"]
      }
    ],
    "selector-type-no-unknown": [
      true,
      {
        "ignoreTypes": ["from"]
      }
    ],
    "property-no-unknown": [
      true,
      {
        "ignoreProperties": ["composes"]
      }
    ],
    "at-rule-no-unknown": [
      true,
      {
        "ignoreAtRules": ["value"]
      }
    ]
  }
}

Styled Components Configuration

{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-styled-components"
  ],
  "processors": ["stylelint-processor-styled-components"],
  "rules": {
    "value-keyword-case": null,
    "declaration-empty-line-before": null
  }
}

Tailwind CSS Configuration

{
  "extends": ["stylelint-config-standard"],
  "rules": {
    "at-rule-no-unknown": [
      true,
      {
        "ignoreAtRules": [
          "tailwind",
          "apply",
          "variants",
          "responsive",
          "screen",
          "layer"
        ]
      }
    ],
    "declaration-block-trailing-semicolon": null,
    "no-descending-specificity": null
  }
}

Build Tool Integration

Webpack Integration

// webpack.config.js
const StylelintPlugin = require('stylelint-webpack-plugin')
 
module.exports = {
  plugins: [
    new StylelintPlugin({
      configFile: '.stylelintrc.json',
      context: 'src',
      files: '**/*.(s(c|a)ss|css)',
      failOnError: false,
      quiet: false,
      emitErrors: true,
      emitWarnings: true,
      fix: false // Set to true to auto-fix issues
    })
  ]
}

Vite Integration

// vite.config.js
import { defineConfig } from 'vite'
import stylelint from 'vite-plugin-stylelint'
 
export default defineConfig({
  plugins: [
    stylelint({
      fix: true,
      include: ['src/**/*.{css,scss,sass,vue}']
    })
  ]
})

Gulp Integration

// gulpfile.js
const gulp = require('gulp')
const stylelint = require('gulp-stylelint')
 
gulp.task('lint-css', () => {
  return gulp
    .src('src/**/*.scss')
    .pipe(stylelint({
      reporters: [
        { formatter: 'verbose', console: true }
      ],
      fix: true
    }))
})
 
gulp.task('watch-css', () => {
  gulp.watch('src/**/*.scss', gulp.series('lint-css'))
})

npm Scripts

{
  "scripts": {
    "lint:css": "stylelint \"src/**/*.{css,scss}\"",
    "lint:css:fix": "stylelint \"src/**/*.{css,scss}\" --fix",
    "lint:css:report": "stylelint \"src/**/*.{css,scss}\" --formatter json --output-file reports/stylelint-report.json"
  }
}

IDE Integration

VS Code Configuration

{
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false,
  "stylelint.enable": true,
  "stylelint.packageManager": "npm",
  "stylelint.validate": ["css", "scss", "sass"],
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": true
  },
  "[css]": {
    "editor.defaultFormatter": "stylelint.vscode-stylelint"
  },
  "[scss]": {
    "editor.defaultFormatter": "stylelint.vscode-stylelint"
  }
}

EditorConfig for CSS

# .editorconfig
root = true
 
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
 
[*.{css,scss,sass}]
indent_style = space
indent_size = 2
 
[*.{json,yml,yaml}]
indent_style = space
indent_size = 2

Custom Rules and Plugins

Custom Stylelint Rule

// stylelint-no-important.js
const stylelint = require('stylelint')
const { utils } = stylelint
 
const ruleName = 'my-org/no-important'
const messages = utils.ruleMessages(ruleName, {
  rejected: 'Avoid using !important'
})
 
module.exports = stylelint.createPlugin(ruleName, (actual) => {
  return (root, result) => {
    const validOptions = utils.validateOptions(result, ruleName, { actual })
    
    if (!validOptions) {
      return
    }
 
    root.walkDecls((decl) => {
      if (decl.important) {
        utils.report({
          message: messages.rejected,
          node: decl,
          result,
          ruleName
        })
      }
    })
  }
})
 
module.exports.ruleName = ruleName
module.exports.messages = messages

BEM Methodology Linting

{
  "extends": ["stylelint-config-standard"],
  "plugins": ["stylelint-selector-bem-pattern"],
  "rules": {
    "plugin/selector-bem-pattern": {
      "preset": "bem",
      "componentName": "[A-Z]+",
      "componentSelectors": {
        "initial": "^\\.{componentName}(?:-[a-z]+)*$",
        "combined": "^\\.combined-{componentName}-[a-z]+$"
      },
      "utilitySelectors": "^\\.util-[a-z]+$"
    }
  }
}

Automated Testing and CI Integration

GitHub Actions

# .github/workflows/css-lint.yml
name: CSS Linting
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  lint-css:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run Stylelint
      run: npm run lint:css
    
    - name: Generate CSS lint report
      if: failure()
      run: |
        npm run lint:css:report
        echo "CSS_LINT_REPORT<<EOF" >> $GITHUB_ENV
        cat reports/stylelint-report.json >> $GITHUB_ENV
        echo "EOF" >> $GITHUB_ENV
    
    - name: Comment PR with lint results
      if: failure() && github.event_name == 'pull_request'
      uses: actions/github-script@v7
      with:
        script: |
          const report = JSON.parse(process.env.CSS_LINT_REPORT || '[]')
          let comment = '## CSS Linting Results\n\n'
          
          if (report.length === 0) {
            comment += '✅ No CSS linting errors found!'
          } else {
            comment += '❌ CSS linting errors found:\n\n'
            report.forEach(file => {
              if (file.warnings.length > 0) {
                comment += `**${file.source}:**\n`
                file.warnings.forEach(warning => {
                  comment += `- Line ${warning.line}: ${warning.text}\n`
                })
                comment += '\n'
              }
            })
          }
          
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: comment
          })

Pre-commit Hooks

# Install husky and lint-staged
npm install --save-dev husky lint-staged
 
# Setup husky
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
{
  "lint-staged": {
    "*.{css,scss,sass}": [
      "stylelint --fix",
      "git add"
    ]
  }
}

Error Detection and Common Issues

Common CSS Errors Detected

/* Syntax errors */
.invalid-property {
  colr: red; /* Property misspelled */
  background-color: #ff000; /* Invalid hex color */
  margin: 10; /* Missing unit */
  padding: 10px 20px 30px 40px 50px; /* Too many values */
}
 
/* Structure errors */
.unclosed-rule {
  color: blue;
  /* Missing closing brace */
 
.nested-too-deep {
  .level1 {
    .level2 {
      .level3 {
        .level4 {
          /* Exceeds max nesting depth */
          color: red;
        }
      }
    }
  }
}
 
/* Performance issues */
.performance-issue {
  color: red !important; /* Avoid !important */
  background-image: url('very-large-image.jpg'); /* Large images */
}
 
/* Accessibility issues */
.accessibility-issue {
  color: #ffffff;
  background-color: #ffffffe; /* Poor contrast */
  font-size: 8px; /* Too small for readability */
}

Linting Report Analysis

// stylelint-reporter.js
const fs = require('fs')
 
class StylelintReporter {
  static analyzeReport(reportPath) {
    const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'))
    
    const analysis = {
      totalFiles: report.length,
      totalWarnings: 0,
      totalErrors: 0,
      ruleBreakdown: {},
      fileBreakdown: []
    }
    
    report.forEach(file => {
      const fileAnalysis = {
        file: file.source,
        warnings: file.warnings.length,
        errors: file.errored ? 1 : 0,
        rules: {}
      }
      
      file.warnings.forEach(warning => {
        if (warning.severity === 'error') {
          analysis.totalErrors++
          fileAnalysis.errors++
        } else {
          analysis.totalWarnings++
        }
        
        // Count rule occurrences
        const rule = warning.rule
        analysis.ruleBreakdown[rule] = (analysis.ruleBreakdown[rule] || 0) + 1
        fileAnalysis.rules[rule] = (fileAnalysis.rules[rule] || 0) + 1
      })
      
      analysis.fileBreakdown.push(fileAnalysis)
    })
    
    return analysis
  }
  
  static generateSummary(analysis) {
    console.log('=== Stylelint Report Summary ===')
    console.log(`Files analyzed: ${analysis.totalFiles}`)
    console.log(`Total warnings: ${analysis.totalWarnings}`)
    console.log(`Total errors: ${analysis.totalErrors}`)
    
    console.log('\n=== Most Common Issues ===')
    const sortedRules = Object.entries(analysis.ruleBreakdown)
      .sort(([,a], [,b]) => b - a)
      .slice(0, 10)
    
    sortedRules.forEach(([rule, count]) => {
      console.log(`${rule}: ${count} occurrences`)
    })
    
    console.log('\n=== Files with Most Issues ===')
    const sortedFiles = analysis.fileBreakdown
      .sort((a, b) => (b.warnings + b.errors) - (a.warnings + a.errors))
      .slice(0, 10)
    
    sortedFiles.forEach(file => {
      console.log(`${file.file}: ${file.warnings + file.errors} issues`)
    })
  }
}
 
// Usage
const analysis = StylelintReporter.analyzeReport('reports/stylelint-report.json')
StylelintReporter.generateSummary(analysis)

Performance Optimization

Large Project Optimization

{
  "extends": ["stylelint-config-standard"],
  "cache": true,
  "cacheLocation": "node_modules/.cache/stylelint",
  "ignoreFiles": [
    "node_modules/**/*",
    "dist/**/*",
    "**/*.min.css",
    "vendor/**/*"
  ],
  "rules": {
    "max-line-length": null,
    "no-descending-specificity": null
  }
}

Parallel Processing

// parallel-stylelint.js
const { Worker } = require('worker_threads')
const path = require('path')
const glob = require('glob')
 
class ParallelStylelint {
  constructor(options = {}) {
    this.workerCount = options.workers || require('os').cpus().length
    this.configFile = options.configFile || '.stylelintrc.json'
  }
  
  async lintFiles(pattern) {
    const files = glob.sync(pattern)
    const chunks = this.chunkArray(files, Math.ceil(files.length / this.workerCount))
    
    const workers = chunks.map(chunk => 
      new Promise((resolve, reject) => {
        const worker = new Worker(path.join(__dirname, 'stylelint-worker.js'), {
          workerData: { files: chunk, configFile: this.configFile }
        })
        
        worker.on('message', resolve)
        worker.on('error', reject)
        worker.on('exit', (code) => {
          if (code !== 0) {
            reject(new Error(`Worker stopped with exit code ${code}`))
          }
        })
      })
    )
    
    const results = await Promise.all(workers)
    return results.flat()
  }
  
  chunkArray(array, chunkSize) {
    const chunks = []
    for (let i = 0; i < array.length; i += chunkSize) {
      chunks.push(array.slice(i, i + chunkSize))
    }
    return chunks
  }
}
 
// Usage
const linter = new ParallelStylelint({ workers: 4 })
linter.lintFiles('src/**/*.scss').then(results => {
  console.log('Linting completed:', results)
})

Best Practices

  1. Start Early: Integrate linting from the beginning of the project
  2. Team Standards: Establish consistent rules across the team
  3. Gradual Adoption: Introduce rules gradually to avoid overwhelming errors
  4. Auto-fix: Use auto-fixing capabilities for simple issues
  5. IDE Integration: Enable real-time linting in code editors
  6. CI Integration: Include linting in continuous integration pipelines
  7. Documentation: Document custom rules and style guidelines
  8. Regular Updates: Keep linting tools and configurations updated
  9. Performance: Optimize linting configuration for large projects
  10. Education: Train team members on CSS best practices and linting rules

Verification

Automated Checks

  • Confirm the computed styles match the intended fix in DevTools.
  • If the rule affects motion, contrast, or layout stability, verify those user-facing outcomes directly.

Manual Checks

  • Inspect the rendered UI at the breakpoints and interaction states affected by the rule.
  • Test at least one mobile and one desktop viewport before shipping.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Set up CSS/SCSS linting tools like Stylelint to automatically detect syntax errors, enforce coding standards, and maintain consistent code quality.

Fix

Auto-fix issues

Configure Stylelint with appropriate rules and integrate it into your build process and code editor for real-time error detection and fixing.

Explain

Learn more

Explain how CSS linting prevents errors, enforces consistent coding standards, and improves maintainability of stylesheets across the team.

Review

Code review

Review stylesheets, component styles, and responsive states related to Lint CSS and SCSS files. Flag exact selectors, declarations, or breakpoints that violate the rule in the rendered UI.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Chrome DevTools
developer.chrome.comTool

Rules that often go hand-in-hand with this one.

Avoid embedded and inline CSS

Embedded and inline CSS are avoided except for critical CSS and performance optimization.

CSS
Include a print stylesheet

A print stylesheet is provided and correctly optimized for printed pages.

CSS
Lint JavaScript code

JavaScript code is linted with ESLint to detect errors and enforce coding standards.

JavaScript
Use @layer to manage CSS cascade order explicitly

CSS Cascade Layers (@layer) are used to give the codebase explicit, predictable control over specificity and cascade order, eliminating the need to fight specificity with !important.

CSS

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385