Slide 1

Slide 1 text

CSS Linter の現在地 2025 年のベストプラクティスを探る まっつー / @ryo_manba 2025/09/21 フロントエンドカンファレンス東京 2025

Slide 2

Slide 2 text

自己紹介 まっつー メルカリ フロントエンドエンジニア Stylelint チームメンバー 𝕏: @ryo_manba GitHub: @ryo-manba 2

Slide 3

Slide 3 text

Quiz: この CSS どう思いますか? .test-1 { colro: red; padding: 16pp; } .test-2 { background-color: red; background: url("images/bg.gif") no-repeat left top; } .test-3 { field-sizing: content; } 3

Slide 4

Slide 4 text

Linter にかけてみる 4

Slide 5

Slide 5 text

正当性の問題 CSS の仕様的に「間違っているもの」を検出 colro → 未知のプロパティ 16pp → 未知の単位 .test-1 { colro: red; padding: 16pp; } 5

Slide 6

Slide 6 text

保守性の問題 仕様的に問題ないが、バグの温床になるパターンを検出 ショートハンドの上書き → background-color: red が無効に .test-2 { background-color: red; background: url("images/bg.gif") no-repeat left top; /* background-color は transparent にリセットされる */ } 6

Slide 7

Slide 7 text

互換性の問題 サポート対象のブラウザで使えない機能を検出 field-sizing: content; → 現状は Chromium 系のみ。Safari / Firefox は未実装 .test-3 { field-sizing: content; } 7

Slide 8

Slide 8 text

CSS Linter がやること 正当性 タイポ/ 構文/ 値の誤りの検出 保守性 変更に弱い書き方の検出(上書き/ 重複/ 不要指定など) 互換性 サポート対象に基づく利用可否の検出 8

Slide 9

Slide 9 text

This talk 2025 年時点の Stylelint / Biome / ESLint を横断比較し、 最適な CSS Linter の“ 選定と運用” の勘所を解説します。 9

Slide 10

Slide 10 text

目次 ツールの現在地と推奨構成(Stylelint / Biome / ESLint ) カスタマイズのしやすさ Tailwind ・CSS Modules ・CSS-in-JS ・SCSS 互換性の基準(Baseline / browserslist ) MCP との組み合わせ 段階的な導入 10

Slide 11

Slide 11 text

ツールの現在地と推奨構成 11

Slide 12

Slide 12 text

Stylelint: 特徴 100 を超える builtin ルール + 豊富なエコシステム 誤検知を極力避ける設計 → 警告が出たら迷わずに直せる Plugin に委ねる領域 文脈依存/ 助言的(a11y 、互換性、フォーマット) 12

Slide 13

Slide 13 text

Stylelint: 推奨構成 recommended = 壊さない最小セット CSS 仕様的な不正(無効値・未知な構文 など)を検出 standard =recommended + モダンな表記の統一 例:ベンダープレフィックス禁止 / kebab-case 命名 / range の context 記法 ( @media (width >= 768px) ) // 壊さない土台 { "extends": ["stylelint-config-recommended"] } // 規約まで含める { "extends": ["stylelint-config-standard"] } 13

Slide 14

Slide 14 text

ESLint: 特徴 2025/02 に CSS を公式サポート(@eslint/css ) 10 を超える built in ルール プロパティ/ At-rule の妥当性 ( no-invalid-* 系) Baseline 準拠 ( use-baseline ) Cascade Layers など最新CSS 潮流にも対応 14

Slide 15

Slide 15 text

ESLint: 推奨構成 css/recommended で 正当性+互換性の土台を一括で有効化 import { defineConfig } from "eslint/config"; import css from "@eslint/css"; export default defineConfig([ { files: ["**/*.css"], language: "css/css", plugins: { css }, extends: ["css/recommended"], }, ]); 15

Slide 16

Slide 16 text

Biome: 特徴 Lint / Format を同梱(1 ツールで整形+検証) まずは 壊さない土台優先 Stylelint の recommended のルールを実装 自前パーサ+データを一元管理 tokenizer / parser / プロパティ・At-rule 定義を内包 → 依存が少 ない 16

Slide 17

Slide 17 text

Biome: 推奨構成 Recommended 相当は linter.enabled をオンにすれば適用 { "css": { "linter": { "enabled": true } } } 17

Slide 18

Slide 18 text

3 つのツール比較 Stylelint Biome ESLint リリース 2015 年 2023 年 2025 年2 月 言語 JavaScript Rust JavaScript ルール数 100+ 20+ 10+ 特徴 CSS 専門 豊富なエコシステム 高速 Formatter 同梱 一元管理 シンプルな設定 一元管理 18

Slide 19

Slide 19 text

カスタマイズのしやすさ 19

Slide 20

Slide 20 text

二種類のカスタマイズ built-in ルールの柔軟性 誤検知が出た箇所のピンポイントな無効化 固有の Property / At-rule の許容など、細かく調整できるか 独自ルールの追加しやすさ 20

Slide 21

Slide 21 text

ルール設計から見るカスタマイズ性の違い 21

Slide 22

Slide 22 text

At-rule の validation 、これだけで大丈夫? @charset "UTF-8"; /* OK */ @foo; /* NG ? */ 未知の At-rule を弾くだけで十分に見えるが、それだけでは不十分 22

Slide 23

Slide 23 text

実際に必要な「4 つの正当性」 at-rule 名 → @foo のような未知 at-rule を禁止 prelude → @property --x {} の --x が妥当か descriptor → @counter-style foo { bar: red; } の bar が妥当か descriptor value → @counter-style foo { system: baz; } の baz が妥当か At-rule の妥当性チェックは この4 軸 が必要。 23

Slide 24

Slide 24 text

At-rule 検証の設計差 Stylelint: 4 つの個別ルールに分割して検出 ルールごとに secondary options (例: ignoreAtRules )でピン ポイント除外 ESLint: 1 つのルールでまとめて検出 言語定義( languageOptions.customSyntax )拡張で誤検知を減ら す Biome: at-rule 名の validation のみ オプションなし 24

Slide 25

Slide 25 text

Stylelint の「細粒度」なルール設計 { "rules": { // 1) 未知の at-rule を禁止(Tailwind 等は個別に逃がす) "at-rule-no-unknown": [true, { "ignoreAtRules": ["tailwind", "apply", "screen", "theme", "layer"] }], // 2) at-rule の不正な prelude を禁止(必要なら特定 at-rule を除外) "at-rule-prelude-no-invalid": [true, { "ignoreAtRules": ["property"] }], // 3) at-rule 内の未知 descriptor を禁止 "at-rule-descriptor-no-unknown": true, // 4) at-rule 内の未知の descriptor の値を禁止 "at-rule-descriptor-value-no-unknown": true } } 25

Slide 26

Slide 26 text

ESLint: 言語定義で“ 許容範囲” を広げる export default defineConfig([ { //... languageOptions: { customSyntax: { // @my-at-rule "hello world!"; が正しい at-rule として認識される atrules: { "my-at-rule": { prelude: "", }, }, }, }, }, ]); 26

Slide 27

Slide 27 text

ルール設計のまとめ ツール 基本方針 オプション 誤検知への対処 Stylelint 検出軸を細分化 (ルール分割) あり ( ignoreAtRules 等) オプション /* stylelint-disable */ ESLint/css 1 ルールで包括 少なめ 言語定義拡張で対応 customSyntax で拡張 /* eslint-disable */ Biome Stylelint の recommended 相当を 実装中 未実装 /* biome-ignore */ 27

Slide 28

Slide 28 text

カスタムルールの追加のしやすさ 28

Slide 29

Slide 29 text

柔軟に独自ルールを足せる価値 組織のナレッジをルール化して再現性を高める レビュー負荷を定常的に削減 コーディングガイドだと抜け漏れが生まれる 29

Slide 30

Slide 30 text

Stylelint :プラグインで実装 特徴:PostCSS AST を直接扱える export default stylelint.createPlugin("plugin/no-foo", () => { return (root, result) => { root.walkRules((rule) => { if (rule.selector.includes("foo")) { stylelint.utils.report({ result, ruleName: "plugin/no-foo", message: ' セレクタに "foo" は使用禁止', node: rule, }); } }); }; }); 30

Slide 31

Slide 31 text

ESLint :カスタムルールで実装 特徴:ESLint の作法で書ける create(context) { return { Rule(node) { const selector = context.sourceCode.getText(node.prelude); if (selector.includes("foo")) { context.report({ node: node.prelude, message: ' セレクタに "foo" は使用禁止', }); } }, }; } 31

Slide 32

Slide 32 text

ESLint :簡易的に独自ルールを作成 no-restricted-syntax で簡単に追加できる { rules: { "no-restricted-syntax": ["error", { selector: "Declaration[important=true]", message: "!important は使用禁止" }] } } 32

Slide 33

Slide 33 text

Biome :GritQL でルール記述 language css; `$selector { $props }` where { $selector <: r".*foo.*", register_diagnostic( span = $selector, message = " セレクタに 'foo' は使用禁止" ) } 注意: GritQL はまだ alpha 版:v0.1.0-alpha.1743007075 最終リリース:2025/03/27 33

Slide 34

Slide 34 text

カスタムルール開発の比較 項目 Stylelint ESLint Biome 学習コ スト 中(PostCSS AST ) 低〜中(ESLint 流儀) 高(DSL 学習が必要) 開発体 験 CSS 専門API が 充実 JS 開発者に馴染 む パターンマッチ / 直 感的な記述 34

Slide 35

Slide 35 text

互換性の基準(Baseline / browserslist ) 35

Slide 36

Slide 36 text

State of CSS 2025 から見える“ 互換性” の課題 Browser support が多くのカテゴリでペインポイントの上位に Interactions: 1 位(16%) Other: 1 位(19%) Typography: 2 位(13%) Layout: 3 位(9%) Shapes & Graphics: 3 位(9%) Colors: 3 位(7%) ref: https://2025.stateofcss.com/en-US/features 36

Slide 37

Slide 37 text

互換性チェックは 2 つのアプローチ browserslist 基準:サポートブラウザに合わせる Baseline 基準:Web 全体で安定して使える時期で揃える 37

Slide 38

Slide 38 text

browserslist で守る(Stylelint ) プラグイン: stylelint-no-unsupported-browser-features を使用 目標ブラウザに未対応な機能を警告 { "plugins": ["stylelint-no-unsupported-browser-features"], "rules": { "plugin/no-unsupported-browser-features": [ true, { "browsers": ["last 1 chrome version"], } ] } } 38

Slide 39

Slide 39 text

Baseline というもう一つの基準 Web 標準の成熟度を 3 段階で表す Limited / Newly available / Widely available (30 ヶ月以上) 対応ブラウザ一覧ではなく、広く安全に使えるしきい値で判断 ツール 実装状況 ESLint Built-in Stylelint Plugin Biome 未実装 39

Slide 40

Slide 40 text

Baseline の設定例 builtin ルールで提供されている { rules: { // widely / newly / 年指定(例: 2024 ) "css/use-baseline": ["warn", { available: "widely" }] } } 40

Slide 41

Slide 41 text

Stylelint: Baseline の設定例 (plugin) ESLint と同じ I/F で設定可能 { plugins: ["stylelint-plugin-use-baseline"], rules: { "plugin/use-baseline": [true, { available: "widely" }] }, } 41

Slide 42

Slide 42 text

まとめ(互換性) まず管理軸を決める 既に browserslist を運用 → Stylelint + no-unsupported-browser- features 機能の安定性ベース → Baseline (ESLint or Stylelint) 42

Slide 43

Slide 43 text

Tailwind ・CSS Modules ・CSS-in-JS ・SCSS 43

Slide 44

Slide 44 text

Tailwind CSS おさらい 送信 送信 小さな単機能のユーティリティクラスを組み合わせる p-4 = padding 、 bg-red-500 = 背景色、 text-xl = 文字サイズ 44

Slide 45

Slide 45 text

クラス文字列は CSS Linter の対象外 // JSX の中のクラス文字列
通常の CSS Linter は CSS の構文(宣言, at-rule など) を解析。 ただの文字列であるクラス( p-2 p-3 )は、そのままでは対象外。 /* CSS Linter が期待する形式 */ .button { padding: 0.5rem; /* ← property: value の宣言 */ background-color: red; } 45

Slide 46

Slide 46 text

eslint-plugin-tailwindcss の登場 発想の転換:CSS 宣言ではなく「クラス名の語彙」を検査 // 検出できる問題
// padding の競合
// 存在しないクラス名 // 自動修正も可能
// → m-5 ( 短縮形)
// → pb-4 pt-2 ( 推奨順序にソート) JSX, Vue, HTML... 幅広くサポート Tailwind CSS v4 対応中 (beta で公開) 46

Slide 47

Slide 47 text

従来の CSS Linter と TailwindCSS 47

Slide 48

Slide 48 text

Stylelint × Tailwind CSS stylelint-config-tailwindcss で誤検知を回避 @tailwind , @apply , theme() など Tailwind 構文でエラーを出さない 通常の CSS を書く領域 (Custom CSS, Global CSS) に適用し品質担保 export default { extends: ["stylelint-config-tailwindcss"] }; 48

Slide 49

Slide 49 text

ESLint + tailwind-csstree languageOptions.customSyntax で Tailwind 構文を追加し、 @eslint/css が 未知扱いせず構文検証が可能に import { tailwind4 } from "tailwind-csstree"; export default defineConfig([ { language: "css/css", plugins: { css }, languageOptions: { customSyntax: tailwind4 } }, ]); 49

Slide 50

Slide 50 text

Tailwind 構文を適切に読み込む @tailwind base; /* 正しい値 */ @tailwind foo; /* 不正な値を検出 */ @apply text-white bg-blue-500; /* 正しい構文 */ @apply { color: red; } /* 無効な @apply 構文 */ a { background: theme(colors.blue.500); /* 正しい関数呼び出し */ color: theme(fake.value); /* 不正なキー */ } 50

Slide 51

Slide 51 text

Biome × Tailwind CSS ルール: useSortedClasses (クラスの並び順を整える) 開発中のため部分的な実装 -
; +
; 51

Slide 52

Slide 52 text

Tailwind CSS サポートまとめ ツール クラス文字列 Tailwind 構文 eslint-plugin-tailwindcss ◎ 専門 × ESLint + tailwind-csstree × ◎ 構文解釈 Stylelint × △ 誤検知の抑制 Biome △ ソート × クラス文字列の検査 と CSS 構文の検査 を分けて設計するのが ◎ 52

Slide 53

Slide 53 text

CSS Modules の対応 拡張構文を使わないなら、どの Linter でも問題なし 独自構文( @value , :export , composes など)を使う場合は、ツー ルごとに対応差あり 53

Slide 54

Slide 54 text

CSS Modules: サポート比較 機能 / 構文 Stylelint (+stylelint-config- css-modules ) Biome @value 対応 対応 composes: 対応 対応 compose-with: 対応 未対応 :local() 対応 未対応 :global() 対応 対応 :export 対応 未対応 ※ ESLint は全て未対応。 @value を使うとパースエラーになる。 54

Slide 55

Slide 55 text

CSS-in-JS の対応 Stylelint のみ可能 Biome, ESLint は未実装 import styled from "styled-components"; /* CSS エラーを含む例 */ const ErrorButton = styled.button` foo: bar; /* 未知プロパティ */ color: baz; /* 不正な値 */ width: 100zz; /* 未知の単位 */ `; 55

Slide 56

Slide 56 text

Stylelint × CSS-in-JS customSyntax を指定して、JSX 内の CSS を lint 可能に { "customSyntax": "postcss-styled-syntax", } ❯ npx stylelint Button.jsx Button.jsx 5:3 Unexpected unknown property "foo" property-no-unknown 6:10 Unexpected unknown value "baz" for property "color" declaration-property-value-no-unknown 7:10 Unexpected unknown value "100zz" for property "width" declaration-property-value-no-unknown 7:13 Unexpected unknown unit "zz" unit-no-unknown 4 problems (4 errors, 0 warnings) 56

Slide 57

Slide 57 text

Less / Sass / SCSS 対応 ツール SCSS Less Sass 備考 Stylelint 共有設定 or customSyntax で対応 ESLint SCSS サポートの PR は出てるが止まっている Biome 57

Slide 58

Slide 58 text

MCP との組み合わせ 58

Slide 59

Slide 59 text

MCP サポート状況 ESLint :公式 MCP サーバーあり( eslint --mcp ) Stylelint :コミュニティ製 stylelint-mcp /公式移管は議論中 Biome :MCP サーバーは未提供(RFC あり) 。 JS API Bindings (alpha ) で自作は可能 59

Slide 60

Slide 60 text

MCP を使うべき理由 AI Agent に 「lint して直して」と依頼しても ルール誤認・見落とし が発生することも プロンプト往復が減る: LLM→Tool→結果、で無駄な“ 推測” を排除 Linter を実行させると時間がかかる 60

Slide 61

Slide 61 text

MCP により精度と再現性が向上 61

Slide 62

Slide 62 text

段階的な導入 62

Slide 63

Slide 63 text

段階的な導入(小さく始めて広げる) 1. Linter の導入 2. ルールを 1 つ追加 3. 差分を修正(or 抑制) 4. 次のルールへ(2 に戻る) 63

Slide 64

Slide 64 text

現実の壁 レガシーコードは修正の影響範囲が不明 Global CSS は特にリスクが高い 一つのエラー修正にも膨大な確認コスト → 導入が進まない 64

Slide 65

Slide 65 text

Bulk Suppressions ESLint v9.24 の新機能 既存の違反を記録し、新規だけ検出する 65

Slide 66

Slide 66 text

実例: 初回実行 /* style.css */ a {} /* 空のブロック */ a { foo: red; /* 未知のプロパティ */ } $ npx eslint style.css style.css 2:3 error Unexpected empty block found css/no-empty-blocks 5:3 error Unknown property 'foo' found css/no-invalid-properties 2 problems (2 errors, 0 warnings) 66

Slide 67

Slide 67 text

実例: Suppressions 生成 # 既存違反を一括抑制 $ npx eslint . --suppress-all // 生成される `eslint-suppressions.json` { "style.css": { "css/no-empty-blocks": { "count": 1 }, "css/no-invalid-properties": { "count": 1 } } } ファイル× ルール× 件数のカウントが json に記録される 67

Slide 68

Slide 68 text

実例: Suppressions が適用される 再実行すると... $ npx eslint style.css # エラーなし! 68

Slide 69

Slide 69 text

実例: 新規違反は検出 a {} /* 既存: 抑制済み */ a { foo: red; /* 既存: 抑制済み */ foo: red; /* ⬅︎ 新規追加 */ } $ npx eslint style.css style.css 5:3 error Unknown property 'foo' found css/no-invalid-properties 6:3 error Unknown property 'foo' found css/no-invalid-properties 2 problems (2 errors, 0 warnings) 69

Slide 70

Slide 70 text

Bulk suppressions を利用した導入 1. 現在のエラー → JSON に保存 2. 新規コードは厳格にチェック 3. 既存エラーは自分のペースで修正 → 今すぐルールを有効化できる 70

Slide 71

Slide 71 text

注意点: IDE ではエラーが表示される IDE 連携は未対応(VS Code 等では既存違反が見え続ける) CI では落ちないが、実装時のノイズに 対応状況: RFC 提案中 将来は抑制済みを別の色やヒントとして表示する案あり 71

Slide 72

Slide 72 text

各ツールの Bulk suppressions の対応状況 ツール 状況 備考 ESLint 利用可能 v9.24+ / IDE 未対応 Stylelint 実装中 PR #8564 Biome 未対応 (JS 系のコメント挿入は別機能) 72

Slide 73

Slide 73 text

Suppressions なしで段階導入 A. overrides で新規だけ厳しく // eslint.config.js export default [{ files: ["src/new/**/*.css"], rules: { "css/no-invalid-properties": "error" } }]; B. コメントで局所的に回避 /* stylelint-disable no-empty-blocks */ a {} /* stylelint-enable */ 73

Slide 74

Slide 74 text

まとめ 74

Slide 75

Slide 75 text

まとめ Stylelint 規約系・細粒度な調整・SCSS/Less ・CSS-in-JS まで面倒見が良い ESLint (@eslint/css ) 妥当性+Baseline をまず担保。単一 ESLint 運用にフィット。 Biome Lint/Format 一体・高速。まずは “ 壊さない土台” から始めたいとき に軽快(機能は拡充中) 。 75