Kurs HTML i CSS · Moduł 9: Nowoczesny CSS

Stylelint - linter dla CSS

10 min czytania ·
W tej lekcji6

W zespole pięciu budowniczych każdy pisze CSS po swojemu: jeden wkleja ten sam selektor dwa razy, drugi zagnieżdża style na siedem poziomów. Przegląd kodu zamienia się w spór o drobiazgi, a prawdziwe błędy przechodzą niezauważone. Stylelint to potężne narzędzie do analizy statycznej kodu CSS, czyli sprawdzania go bez uruchamiania strony. Pomaga w utrzymaniu spójności, wykrywaniu błędów i egzekwowaniu dobrych praktyk w arkuszach stylów.

Podstawy Stylelint

Instalacja i konfiguracja

Stylelint instalujesz jako zależność deweloperską projektu, razem z gotowym zestawem reguł i opcjonalnymi wtyczkami:

# Instalacja podstawowa
npm install --save-dev stylelint stylelint-config-standard

# Z dodatkowymi pluginami
npm install --save-dev stylelint-scss stylelint-order stylelint-config-prettier

Pakiet stylelint-config-standard to rozsądny zestaw reguł na start, stylelint-scss uczy Stylelinta składni SCSS, a stylelint-order pilnuje kolejności właściwości. Pakiet stylelint-config-prettier jest od Stylelinta 15 zbędny.

Podstawowy plik konfiguracyjny .stylelintrc.json

Konfigurację zapisujesz w pliku .stylelintrc.json. Pole extends wczytuje gotowy zestaw, plugins dodaje wtyczki, a rules włącza, wyłącza lub dostraja pojedyncze reguły:

{
  "extends": [
    "stylelint-config-standard"
  ],
  "plugins": [
    "stylelint-order",
    "stylelint-scss"
  ],
  "rules": {
    "indentation": 2,
    "string-quotes": "single",
    "no-duplicate-selectors": true,
    "color-hex-case": "lower",
    "color-hex-length": "short",
    "color-named": "never",
    "selector-combinator-space-after": "always",
    "selector-combinator-space-before": "always",
    "selector-pseudo-element-colon-notation": "double",
    "selector-pseudo-class-parentheses-space-inside": "never",
    "selector-no-qualifying-type": true,
    "property-no-unknown": true,
    "keyframe-declaration-no-important": true,
    "declaration-block-trailing-semicolon": "always",
    "declaration-no-important": true,
    "declaration-block-single-line-max-declarations": 1,
    "selector-class-pattern": "^[a-z0-9\\-]+$",
    "selector-id-pattern": "^[a-z0-9\\-]+$",
    "custom-property-pattern": "^[a-z0-9\\-]+$",
    "max-nesting-depth": 3,
    "order/order": [
      "custom-properties",
      "declarations"
    ],
    "order/properties-order": [
      "position",
      "top",
      "right",
      "bottom",
      "left",
      "z-index",
      "display",
      "flex",
      "flex-direction",
      "flex-wrap",
      "justify-content",
      "align-items",
      "width",
      "height",
      "margin",
      "padding",
      "background",
      "color",
      "font-family",
      "font-size",
      "line-height",
      "border",
      "border-radius",
      "transition",
      "transform"
    ]
  }
}

Ważna uwaga: reguły czysto stylistyczne, takie jak indentation, string-quotes, color-hex-case czy declaration-block-trailing-semicolon, zostały oznaczone jako przestarzałe w Stylelincie 15 i usunięte w wersji 16, która zgłosi je jako nieznane. Usuń je albo przenieś do wtyczki @stylistic/stylelint-plugin, a formatowanie zostaw Prettierowi. Reguły wykrywające błędy, jak no-duplicate-selectors, działają bez zmian.

Rozszerzona konfiguracja dla większych projektów

W większym projekcie dochodzą wtyczki do konwencji nazw, wymuszanie zmiennych i osobne reguły dla różnych typów plików:

{
  "extends": [
    "stylelint-config-standard",
    "stylelint-config-prettier"
  ],
  "plugins": [
    "stylelint-order",
    "stylelint-scss",
    "stylelint-selector-bem-pattern",
    "stylelint-declaration-use-variable"
  ],
  "rules": {
    "plugin/selector-bem-pattern": {
      "preset": "bem",
      "presetOptions": {
        "namespace": "c-"
      }
    },
    "sh-waqar/declaration-use-variable": [
      [
        "color",
        "background-color",
        "border-color",
        "font-family",
        "font-size",
        "z-index"
      ]
    ],
    "color-function-notation": "modern",
    "alpha-value-notation": "percentage",
    "color-hex-alpha": "never",
    "font-family-name-quotes": "always-where-recommended",
    "function-url-quotes": "always",
    "import-notation": "string",
    "keyframe-selector-notation": "percentage-unless-within-keyword-only-block",
    "media-feature-range-notation": "context",
    "selector-not-notation": "complex",
    "at-rule-no-vendor-prefix": true,
    "media-feature-name-no-vendor-prefix": true,
    "property-no-vendor-prefix": true,
    "selector-no-vendor-prefix": true,
    "value-no-vendor-prefix": true,
    "max-line-length": 120,
    "no-empty-source": null,
    "declaration-block-no-redundant-longhand-properties": true,
    "shorthand-property-no-redundant-values": true,
    "comment-empty-line-before": [
      "always",
      {
        "except": ["first-nested"],
        "ignore": ["stylelint-commands"]
      }
    ],
    "custom-property-empty-line-before": [
      "always",
      {
        "except": ["after-custom-property", "first-nested"],
        "ignore": ["after-comment", "inside-single-line-block"]
      }
    ],
    "declaration-empty-line-before": [
      "always",
      {
        "except": ["after-declaration", "first-nested"],
        "ignore": ["after-comment", "inside-single-line-block"]
      }
    ]
  },
  "overrides": [
    {
      "files": ["*.scss", "**/*.scss"],
      "customSyntax": "postcss-scss",
      "rules": {
        "scss/at-rule-no-unknown": true,
        "scss/selector-no-redundant-nesting-selector": true,
        "scss/no-duplicate-dollar-variables": true,
        "scss/dollar-variable-pattern": "^[a-z0-9\\-]+$",
        "scss/percent-placeholder-pattern": "^[a-z0-9\\-]+$"
      }
    },
    {
      "files": ["*.vue", "**/*.vue"],
      "customSyntax": "postcss-html"
    }
  ]
}

Sekcja overrides stosuje dodatkowe reguły do wskazanych plików, a customSyntax pozwala czytać SCSS czy pliki Vue. Tu też usuń przestarzałą regułę max-line-length.

Integracja z narzędziami deweloperskimi

Package.json scripts

Stylelinta uruchamiasz skryptami w package.json, dzięki czemu cały zespół używa tych samych poleceń:

{
  "scripts": {
    "lint:css": "stylelint \"src/**/*.{css,scss}\"",
    "lint:css:fix": "stylelint \"src/**/*.{css,scss}\" --fix",
    "lint:css:report": "stylelint \"src/**/*.{css,scss}\" --formatter json > stylelint-report.json",
    "prebuild": "npm run lint:css",
    "test:css": "stylelint \"src/**/*.{css,scss}\" --formatter tap"
  }
}

Polecenie npm run lint:css tylko sprawdza pliki, a lint:css:fix z flagą --fix automatycznie poprawia te błędy, które narzędzie potrafi naprawić samo. Skrypt prebuild npm uruchomi przed każdym buildem.

Integracja z VS Code (.vscode/settings.json)

Oficjalne rozszerzenie Stylelint do VS Code podkreśla problemy od razu podczas pisania, a te ustawienia je konfigurują:

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": true
  },
  "css.validate": false,
  "less.validate": false,
  "scss.validate": false,
  "stylelint.validate": ["css", "postcss", "scss", "sass"],
  "stylelint.snippet": ["css", "postcss", "scss", "sass"],
  "stylelint.enable": true
}

Ustawienie source.fixAll.stylelint poprawia plik przy każdym zapisie, a wyłączenie wbudowanej walidacji CSS zapobiega podwójnym komunikatom.

Pre-commit hook z Husky

Ostatnia linia obrony to sprawdzenie plików tuż przed commitem, żeby błędny kod nie trafił do repozytorium:

# Instalacja husky i lint-staged
npm install --save-dev husky lint-staged

# package.json
{
  "lint-staged": {
    "*.{css,scss}": [
      "stylelint --fix",
      "git add"
    ]
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  }
}

To zapis dla starszych wersji narzędzi. W aktualnym Husky hook tworzysz poleceniem npx husky init, które zakłada plik .husky/pre-commit, a lint-staged od wersji 10 sam dodaje poprawione pliki, więc git add jest zbędne.

Przykłady reguł i ich zastosowania

1. Formatowanie i spójność

Zobaczmy, jakie problemy wyłapuje linter. Najpierw kod z błędami, a potem jego poprawiona wersja:

/* Błędy wykrywane przez Stylelint */

/* Nieprawidłowe wcięcia */
.component {
margin: 0;
  padding: 1rem;
}

/* Brak spacji */
.component{margin:0;padding:1rem;}

/* Nieprawidłowe quotes */
.component {
  font-family: "Arial";
  background: url(image.jpg);
}

/* Nieprawidłowe kolory */
.component {
  color: RED;
  background: #ffffff;
  border: 1px solid #fff;
}

/* Poprawiony kod zgodny z regułami */

/* Poprawne wcięcia */
.component {
  margin: 0;
  padding: 1rem;
}

/* Poprawne spacje */
.component {
  margin: 0;
  padding: 1rem;
}

/* Poprawne quotes */
.component {
  font-family: 'Arial', sans-serif;
  background: url('image.jpg');
}

/* Poprawne kolory */
.component {
  color: #f00;
  background: #fff;
  border: 1px solid #fff;
}

Wcięcia poprawi dziś Prettier, ale Stylelint nadal wyłapie na przykład nazwane kolory czy zapis #ffffff zamiast #fff.

2. Kolejność właściwości

Wtyczka stylelint-order pilnuje, by właściwości stały zawsze w kolejności ustalonej w konfiguracji:

/* Nieprawidłowa kolejność */
.component {
  color: #333;
  position: absolute;
  background: #fff;
  top: 0;
  margin: 1rem;
  display: flex;
  left: 0;
  padding: 1rem;
}

/* Poprawiona kolejność według konfiguracji */
.component {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  margin: 1rem;
  padding: 1rem;
  background: #fff;
  color: #333;
}

/* Jeszcze lepsza organizacja z grupowaniem */
.component {
  /* Positioning */
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;

  /* Layout */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  /* Box model */
  width: 100%;
  height: 200px;
  margin: 1rem;
  padding: 1rem;

  /* Visual */
  background: #fff;
  color: #333;
  border: 1px solid #ddd;
  border-radius: 4px;

  /* Typography */
  font-family: 'Inter', sans-serif;
  font-size: 1rem;
  line-height: 1.5;

  /* Animation */
  transition: all 0.3s ease;
  transform: translateY(0);
}

Z flagą --fix Stylelint sam przestawi deklaracje. Wygląd strony się nie zmienia, rośnie tylko czytelność.

3. BEM pattern validation

BEM to konwencja nazw: blok (.card), element po dwóch podkreślnikach (.card__title) i modyfikator po dwóch myślnikach (.card--featured). Wtyczka BEM sprawdza, czy klasy trzymają się tego wzoru:

/* Nieprawidłowe nazwy klas BEM */
.Component { } /* PascalCase */
.component_element { } /* single underscore */
.component-element-modifier { } /* dash instead of -- */
.componentElement { } /* camelCase */

/* Poprawne nazwy klas BEM */
.component { }
.component__element { }
.component__element--modifier { }
.component--variant { }
.multi-word-component { }
.multi-word-component__element-name { }
.multi-word-component__element-name--modifier-name { }

Poprawna jest więc .component__element--modifier, a błędne są pojedynczy podkreślnik, PascalCase i camelCase.

4. SCSS specific rules

Dla SCSS wtyczka stylelint-scss sprawdza nazwy zmiennych, placeholdery i mixiny, a reguła max-nesting-depth zbyt głębokie zagnieżdżenia:

/* Nieprawidłowy SCSS */
$primaryColor: #007bff; /* camelCase variable */
$primary_color: #007bff; /* underscore variable */

%button-style { /* nieużywany placeholder */
  padding: 1rem;
}

.component {
  .nested {
    .deep {
      .too {
        .deep { /* zbyt głębokie zagnieżdżenie */
          color: red;
        }
      }
    }
  }
}

@mixin buttonMixin() { /* camelCase mixin */
  padding: 1rem;
}

/* Poprawny SCSS */
$primary-color: #007bff;
$secondary-color: #6c757d;

%button-base {
  padding: 1rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.component {
  @include button-mixin;

  &__element {
    color: $primary-color;

    &--modifier {
      background: $secondary-color;
    }
  }
}

@mixin button-mixin($size: medium) {
  @extend %button-base;

  @if $size == small {
    padding: 0.5rem;
  } @else if $size == large {
    padding: 1.5rem;
  }
}

Zmienne i mixiny zapisane w kebab-case, czyli $primary-color i button-mixin, pasują do nazw właściwości CSS.

Zaawansowana konfiguracja

Custom rules dla specific projektów

Reguły z końcówką -pattern przyjmują wyrażenia regularne, a listy dozwolonych wartości pozwalają zawęzić CSS do potrzeb projektu:

{
  "rules": {
    "comment-pattern": "^[A-Z][\\s\\S]*\\.$",
    "custom-property-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
    "keyframes-name-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*$",
    "selector-class-pattern": [
      "^[a-z]([a-z0-9-]+)?(__([a-z0-9]+-?)+)?(--([a-z0-9]+-?)+){0,2}$",
      {
        "message": "Expected class selector to be kebab-case BEM format"
      }
    ],
    "declaration-property-value-allowed-list": {
      "border": ["/^0$/"],
      "border-top": ["/^0$/"],
      "border-right": ["/^0$/"],
      "border-bottom": ["/^0$/"],
      "border-left": ["/^0$/"]
    },
    "property-allowed-list": [
      "position",
      "top",
      "right",
      "bottom",
      "left",
      "z-index",
      "display",
      "flex",
      "flex-direction",
      "flex-wrap",
      "justify-content",
      "align-items",
      "align-content",
      "gap",
      "width",
      "height",
      "min-width",
      "min-height",
      "max-width",
      "max-height",
      "margin",
      "margin-top",
      "margin-right",
      "margin-bottom",
      "margin-left",
      "padding",
      "padding-top",
      "padding-right",
      "padding-bottom",
      "padding-left",
      "background",
      "background-color",
      "background-image",
      "background-size",
      "background-position",
      "background-repeat",
      "color",
      "font-family",
      "font-size",
      "font-weight",
      "line-height",
      "text-align",
      "text-decoration",
      "text-transform",
      "border",
      "border-top",
      "border-right",
      "border-bottom",
      "border-left",
      "border-radius",
      "box-shadow",
      "opacity",
      "visibility",
      "overflow",
      "overflow-x",
      "overflow-y",
      "cursor",
      "transition",
      "transform",
      "animation"
    ]
  }
}

property-allowed-list dopuszcza tylko wymienione właściwości, więc to reguła bardzo surowa, dobra dla systemów projektowych.

Performance-focused rules

Te reguły ograniczają złożoność selektorów: liczbę identyfikatorów, klas, zagnieżdżeń oraz specyficzność:

{
  "rules": {
    "selector-max-id": 0,
    "selector-max-universal": 1,
    "selector-max-type": 2,
    "selector-max-class": 4,
    "selector-max-attribute": 2,
    "selector-max-pseudo-class": 3,
    "selector-max-compound-selectors": 4,
    "selector-max-specificity": "0,4,0",
    "max-nesting-depth": 3,
    "declaration-block-no-redundant-longhand-properties": true,
    "shorthand-property-no-redundant-values": true,
    "no-unknown-animations": true
  }
}

selector-max-id: 0 zakazuje identyfikatorów w selektorach, a selector-max-specificity ustawia górną granicę.

Integracja z build tools

Webpack integration

Stylelinta podepniesz też do narzędzia budującego, a błędy zobaczysz w konsoli podczas budowania:

// webpack.config.js
const StylelintPlugin = require('stylelint-webpack-plugin');

module.exports = {
  plugins: [
    new StylelintPlugin({
      configFile: '.stylelintrc.json',
      context: 'src',
      files: '**/*.{css,scss}',
      failOnError: false,
      quiet: false,
      emitErrors: true
    })
  ]
};

Przy failOnError: false błędy są raportowane, ale nie przerywają budowania.

Gulp integration

W projektach opartych na Gulpie linter to osobne zadanie, obok zadania naprawiającego pliki:

// gulpfile.js
const gulp = require('gulp');
const stylelint = require('gulp-stylelint');

gulp.task('lint-css', function() {
  return gulp
    .src('src/**/*.{css,scss}')
    .pipe(stylelint({
      reporters: [
        { formatter: 'string', console: true }
      ]
    }));
});

gulp.task('fix-css', function() {
  return gulp
    .src('src/**/*.{css,scss}')
    .pipe(stylelint({
      fix: true
    }))
    .pipe(gulp.dest('src'));
});

Zadanie fix-css zapisuje poprawione pliki z powrotem do katalogu src.

PostCSS integration

Stylelint działa również jako wtyczka PostCSS. Kolejność wtyczek ma znaczenie, bo linter musi zobaczyć kod przed przekształceniem:

// postcss.config.js
module.exports = {
  plugins: [
    require('stylelint')({
      config: require('./.stylelintrc.json')
    }),
    require('autoprefixer'),
    require('cssnano')({
      preset: 'default'
    })
  ]
};

Stylelint sprawdza źródło, Autoprefixer dodaje prefiksy, a cssnano na końcu minifikuje wynik.

CI/CD Integration

GitHub Actions

Na serwerze ciągłej integracji lint uruchamia się przy każdym pushu i pull requeście:

# .github/workflows/css-lint.yml
name: CSS Lint

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '22'

    - name: Install dependencies
      run: npm ci

    - name: Run Stylelint
      run: npm run lint:css

    - name: Upload lint results
      uses: actions/upload-artifact@v4
      if: failure()
      with:
        name: stylelint-results
        path: stylelint-report.json

Wersje akcji co jakiś czas się zmieniają, więc sprawdzaj je w dokumentacji GitHub Actions.

Custom reporting

Przez Node.js API przygotujesz własne podsumowanie, na przykład liczbę błędów według reguł:

// scripts/css-report.js
const stylelint = require('stylelint');
const fs = require('fs');

stylelint.lint({
  files: 'src/**/*.{css,scss}',
  formatter: 'json'
}).then(function(resultObject) {
  const results = JSON.parse(resultObject.output);

  const summary = {
    totalFiles: results.length,
    totalErrors: results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0),
    errorsByRule: {}
  };

  results.forEach(file => {
    file.warnings.forEach(warning => {
      const rule = warning.rule;
      summary.errorsByRule[rule] = (summary.errorsByRule[rule] || 0) + 1;
    });
  });

  console.log('CSS Lint Summary:');
  console.log(`Total files analyzed: ${summary.totalFiles}`);
  console.log(`Total errors found: ${summary.totalErrors}`);
  console.log('Errors by rule:');

  Object.entries(summary.errorsByRule)
    .sort(([,a], [,b]) => b - a)
    .forEach(([rule, count]) => {
      console.log(`  ${rule}: ${count}`);
    });

  fs.writeFileSync('css-lint-summary.json', JSON.stringify(summary, null, 2));
});

Skrypt liczy ostrzeżenia o wadze error, grupuje je według nazwy reguły i zapisuje raport do pliku JSON.

Stylelint jest niezbędnym narzędziem w profesjonalnym developmencie CSS, a właściwa konfiguracja znacząco poprawia jakość i utrzymywalność stylów. Moja rada: wdrażaj go w czterech krokach, czyli instalacja pakietów, plik .stylelintrc.json, skrypt lint:css w package.json i pierwsze uruchomienie. W następnej lekcji sprawdzimy, jak Twoje style wyglądają w różnych przeglądarkach.

Pamiętaj: Stylelint to nadzorca budowy, który ogląda każdy blok kamienia, zanim trafi on na piramidę.

Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>:is() i :where()</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <main>
        <article>
            <h2>Artykul o piramidach</h2>
            <p>Tekst z <a href="#">linkiem</a>.</p>
        </article>
        <section>
            <h2>Sekcja o faraonach</h2>
            <p>Tekst z <a href="#">linkiem</a>.</p>
        </section>
        <nav>
            <h2>Nawigacja</h2>
            <ul>
                <li><a href="#">Link 1</a></li>
                <li><a href="#">Link 2</a></li>
            </ul>
        </nav>
    </main>
</body>
</html>

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym jest Stylelint?

  2. 2. Co robi flaga --fix uruchomiona ze Stylelint?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Ułóż kroki wdrażania Stylelint w projekcie we właściwej kolejności:

Przydatne artykuły