web-styling-scss-modules について
SCSS Modulesでコンポーネントのスタイルを書くためのパターンガイドです。ローカルスコープ、Sassのモジュール構成、カスケードレイヤー、ビルド時の値と実行時のカスタムプロパティの使い分けを解説します。
主な機能
- 01ローカルスコープのモジュールクラス
- 02@useと@forwardによるSass構成
- 03上書きを制御するカスケードレイヤー
- 04状態を表す属性セレクター
- 05ビルド時の値と実行時プロパティ
- 06ミックスインと意図的なグローバル指定
活用シーン
- 01独立したUIコンポーネントのスタイル作成
- 02複数ファイルにわたるSassの整理
- 03アプリ側からライブラリスタイルを上書き
- 04カスタムプロパティによるテーマ対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-styling-scss-modulesインストール後、Claude Code で/web-styling-scss-modulesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/advanced.mdcore.mdmodules.mdpatterns.mdtheming.mdtokens.md詳細
SCSS Modules のパターン
クイックガイド:
*.module.scssファイルは、ローカルスコープのスタイルシートです。クラス名はビルド時に書き換えられ、インポートしたオブジェクトを通じて参照されるため、あるコンポーネントのスタイルがほかのスタイルと衝突することはありません。ライブラリのスタイルは@layer components {}で囲むと、アプリケーション側でレイヤーに属さないルールを使って、詳細度を競うことなく上書きできます。Sass の読み込みには@useと@forwardを使います。@importは非推奨で、今後廃止されます。状態のスタイルには、状態ごとにクラスを用意するのではなく属性セレクターを使い、再ビルドせずに変更する必要がある値は Sass 変数ではなくカスタムプロパティに保持してください。
詳細なリソース:
- examples/core.md — ローカルスコープと
stylesオブジェクト、カスケードレイヤーの設定、レイヤー化されたコンポーネントスタイル、レイヤーに属さないアプリケーション側のオーバーライド - examples/modules.md —
@use、@forward、名前空間、withによる設定、組み込みモジュール、@importからの移行 - examples/patterns.md — モジュールのファイル構成、データ属性による状態管理、ミックスイン、グローバルスタイル、アイコンのサイズ設定
- examples/tokens.md — モジュールスタイルからのデザイントークンの読み取り、コンポーネントスコープのカスタムプロパティ
- examples/theming.md — モジュール内でテーマ分岐を避ける方法
- examples/advanced.md —
:has()、:global()、ネストの深さ - reference.md — 修正方法を含むアンチパターン、
@useと@importの比較、Sass の非推奨に関する注意事項
<critical_requirements>
SCSS Modules コードを書く前に
すべてのクラスには、インポートしたオブジェクトを通じてアクセスします。 スタイルシート内の名前は、ブラウザーが受け取る CSS 内の名前とは異なるため、リテラルの className="button" は何にも一致しません。エラーも、スタイルも、手がかりもありません。
ライブラリコンポーネントのスタイルは @layer components {} で囲みます。 そうすれば、アプリケーション側は通常のレイヤー外ルールで上書きできます。詳細度を競い合う必要がなく、どちらのスタイルシートが先に読み込まれたかにも左右されません。
Sass ファイルの読み込みには @use を使います。 読み込んだ内容に名前空間を付け、コンパイルごとに各ファイルを一度だけ評価し、_ で始まるメンバーを非公開にします。@import にはこれらの機能がなく、Dart Sass 1.80.0 で非推奨になり、3.0.0 で削除されます。
再ビルドせずに変更する必要がある値は、カスタムプロパティに保持します。 Sass 変数や darken() はコンパイル時に解決されるため、実行時にアクセスする方法はありません。
状態のスタイルは、コンポーネント自身のクラスに対する属性セレクターで指定します。 &[data-state="open"] を使えば、すべての状態をスタイルシート内にまとめられます。状態ごとにクラスを用意すると、コンポーネントの見た目が、それらの状態を選択するマークアップ全体に分散してしまいます。
</critical_requirements>
自動検出: .module.scss、.module.css、styles. クラスアクセス、@use、@forward、@layer components、:global()、@mixin、@include、!default、sass:math、math.div、sass:color、color.scale、Sass 内の @import、sass-migrator
対象:
- ローカルスコープのクラス名を使って
*.module.scssにコンポーネントスタイルを書く @useと@forwardで Sass をファイル間に整理し、@importから移行する- カスケードレイヤーでライブラリスタイルとアプリケーションによる上書きの優先順位を制御する
- ビルド時の Sass 値にするものと、実行時のカスタムプロパティにするものを判断する
- 繰り返し使う宣言をミックスインに抽出する
:global()を使って意図的にモジュールスコープから抜け出す
別の項目で扱う内容:
- どのデザイントークンが存在し、どのように階層化され、何と呼ばれるか — モジュールスタイルは
var()を通じて読み取るだけで、定義はしません - テーマの選択、永続化、実行時の適用方法 — モジュールの役割はテーマ分岐を一切持たないことです。そのため、指示がなくても適応できます
- コンポーネントの props からクラスリストを作る方法 — モジュールはクラス名を エクスポート します。それらを組み合わせる方法は別の問題です
- デザイン領域全体を網羅する生成ユーティリティクラス — モジュールシステムは手書き CSS のスコープを制御しますが、これは別のトレードオフです
スコープ化こそが要点です。一意性をビルドが担うため、コードベース全体を命名規則でまとめなくても、スタイルシートでは短く分かりやすい名前を使えます。BEM 風の接頭辞は不要で、コンポーネントを削除すれば、そのスタイルも削除されます。
ここから二つの帰結が導かれ、このスキルの大半はそれを前提としています。グローバルな処理が偶然起こることはありません。そのため、リセット、レイヤー宣言、デザイントークンなど、本当にグローバルなものは意図的に、モジュールの外に記述します。また、あるモジュールから別のモジュールの名前にアクセスすることはできません。そのため、共有する決定事項は、@use を通じた Sass メンバー、またはカスケードを通じたカスタムプロパティとして共有します。二つのファイルが取り決めたクラス名を介するのではありません。
基本パターン
パターン 1:ローカルスコープ — クラス名がオブジェクトのキーになる
*.module.scss ファイルをインポートすると、そのファイルで宣言されたクラスをキーとし、ビルド時に生成されたグローバルに一意な名前を値とするオブジェクトが返されます。実際に CSS に出力される名前へアクセスする方法は、このオブジェクトだけです。
// button.module.scss
.button {
padding: var(--space-md);
}
.sizeSm {
padding: var(--space-sm);
}
// button.tsx
import styles from "./button.module.scss";
// styles.button is something like "button-module__x7f2q" — never "button"
<button className={`${styles.button} ${styles.sizeSm}`} />;
生成される名前の形式はビルド設定によって決まるため、これに依存してはいけません。キーは記述したとおりのクラス名です。ビルドで camelCase エイリアスを追加する設定にしていない限り、.size-sm には styles["size-sm"] でアクセスします。そのため、これらのスタイルシートでは最初からクラス名に camelCase を使います。
全コード:examples/core.md
パターン 2:予測可能な優先順位のためのカスケードレイヤー
レイヤーの順序を一度だけ宣言し、ライブラリのコンポーネントスタイルは @layer components の中に配置します。アプリケーションのスタイルはレイヤーに入れないことで、読み込み順にかかわらず、カスケードでどのレイヤーよりも優先されます。
// layers.scss — loaded first, before anything it names
@layer reset, components;
// button.module.scss — a library component
@layer components {
.button {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
}
}
// an application stylesheet — no layer, so it wins
.myCustomButton {
background-color: var(--color-accent);
}
全コード:examples/core.md
パターン 3:Sass モジュールシステム
@use はファイルを名前空間の下で読み込み、コンパイルごとに一度だけ読み込みます。@forward は、あるファイルのメンバーを別のファイル経由で再エクスポートします。これにより、ディレクトリに単一のエントリーポイントを設けられます。
// styles/_index.scss — the public surface
@forward "layers";
@forward "design-tokens";
@forward "mixins";
// button.module.scss
@use "../../styles" as s;
@layer components {
.button {
padding: s.$space-md;
@include s.focus-ring;
}
}
@use "…" as * は名前空間を省略でき、どこでも使う基盤モジュールにのみ適しています。with で設定されたモジュールは、コンパイル全体でその設定を維持します。最初の読み込みが優先されます。
全コード:examples/modules.md
パターン 4:ビルド時の Sass 値と実行時のカスタムプロパティ
ブラウザーが CSS を認識する時点では、Sass 変数はすでに存在しません。テーマ、密度設定、メディア条件に応じて変化させる必要があるものは、カスタムプロパティにする必要があります。
@use "sass:math";
// Build-time: a value computed once, that never changes at runtime
$column-width: math.div(100%, 12);
.grid {
grid-template-columns: repeat(12, $column-width);
// Runtime: reassigning this token elsewhere re-styles the element
gap: var(--space-md);
background: color-mix(in srgb, var(--color-primary), black 5%);
}
math.div() は / の代わりに使用します。/ は CSS のスラッシュ区切り文字(grid-template: 1fr / 2fr)と曖昧になるためです。ビルド時に実際に色を計算する場合は、darken() と lighten() の代わりに color.scale() を使用します。ただし、テーマによって変更できる必要がある色はカスタムプロパティにし、color-mix() または相対カラー構文と組み合わせます。
全コード:examples/modules.md
パターン 5:トークンを再宣言せず、読み取る
モジュールは var() を介してデザイントークンを使用します。コンポーネント内で値を変化させる必要がある場合に限り、独自のカスタムプロパティを宣言し、そのコンポーネント自身のクラスに設定してサブツリーの範囲内に適用します。
@layer components {
.button {
// Component-scoped: the variants below reassign it
--button-accent-bg: transparent;
padding: var(--space-md);
font-size: var(--font-size-body);
color: var(--color-text-default);
background: var(--button-accent-bg);
}
.outline:hover {
--button-accent-bg: var(--color-background-muted);
}
}
すでに存在するトークン(--radius-sm)の横でトークンを再宣言する(--card-radius: 0.5rem)と、元の値に追従しない別の値が生まれます。
全コード:examples/tokens.md
パターン 6:状態を表すデータ属性
コンポーネントにはひとつのクラスを使い、状態は属性セレクターで表します。これにより、組み合わせごとにクラスを作らずに状態を組み合わせられます。
.dropdown {
&[data-open="true"] {
display: block;
}
&[data-size="large"][data-variant="primary"] {
padding: var(--space-xlg);
}
&[aria-invalid="true"] {
border-color: var(--color-destructive);
}
}
単独のブール属性ではなく、文字列値(data-state="open")を使用します。属性値セレクターなら、どの状態でも同じように読み取れ、DOM を見ればコンポーネントがどの状態だと認識しているかがわかります。
全コード:examples/patterns.md
パターン 7:繰り返し使う宣言ブロックにはミックスイン
複数のコンポーネントで同じ宣言ブロックが必要な場合は、ミックスインが適しています。特に、部分的なコピーがバグにつながるアクセシビリティ関連の宣言に有効です。
@mixin focus-ring {
&:focus-visible {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
}
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.button {
@include focus-ring;
}
最初ではなく、3つ目の使用箇所で抽出します。一度しか使わないミックスインは、余分な中継を挟んだ CSS ブロックにすぎません。
全コード:examples/patterns.md
パターン 8:テーマに依存しないコンポーネントスタイル
モジュールでは役割に名前を付け、テーマの種類には触れません。スタイルシートは一度だけ記述し、各テーマはすでに読み込んでいるトークンを通じて適用されます。
@layer components {
.button {
background-color: var(--color-primary);
color: var(--color-primary-foreground);
// No `.dark &`, no theme conditional — the tokens carry the mode
&:hover {
background-color: var(--color-primary-hover);
}
}
}
モジュール内の .dark & セレクターは、そのモジュールをひとつのテーマ実装に結び付けます。テーマをもうひとつ追加すると、そのセレクターを持つすべてのコンポーネントに分岐をもうひとつ追加することになります。
全コード:examples/theming.md
パターン 9:意図を持ってエスケープし、ネストする
:global() を使うとセレクターがスコープの対象外になり、サードパーティのクラス名を参照できるようになります。& を使ったネストにより、修飾子を修飾対象のそばに記述できます。
.component {
padding: var(--space-md);
// Reach a class this module does not own
:global(.third-party-widget) & {
padding: 0;
}
// `:has()` styles a parent from its children — no JavaScript needed
&:has(input:focus) {
background: var(--color-background-muted);
}
}
ネストはおよそ3階層までにします。階層が増えるたびに詳細度が上がり、生成されるセレクターがマークアップに記述された名前から遠ざかります。
全コード:examples/advanced.md
<red_flags>
要注意の問題
ビルド時にエラーになります:
- 新しい Sass での
@import— Dart Sass 1.80.0 で非推奨となり、3.0.0 で削除されます。代わりに@useを使用します。sass-migrator module --migrate-depsで大部分を変換できます。 - 除算に
/を使う(100% / 2)— 非推奨であり、CSS のスラッシュ区切りと紛らわしい構文です。sass:mathのmath.div()を使えば明示できます。calc()内の/には影響しません。 - スタイルルールの後に
@useを置く —@forwardと@charsetを除き、すべてに先行する必要があるため、その前にルールがあるとコンパイルエラーになります。モジュールをwithで設定する際に変数宣言を先に置く必要があるため、変数宣言は例外として認められています。 - レイヤー宣言がない —
@layer reset, components;はレイヤー化されたコンテンツより前に評価する必要があります。そうでないと、ファイルの読み込み順によって順序が決まります。
何も適用されず、気づきにくい問題:
- マークアップでクラス名を文字列として記述する(
className="button")— 出力される名前は生成されるため、その文字列に一致するルールはありません。エラーは発生せず、要素にはスタイルが適用されません。 - ハイフン区切りのクラス名をプロパティとして読み取る(
styles.size-sm)— これは検索ではなく、減算として解析されます。クラス名はキャメルケースで記述するか、styles["size-sm"]を使用してください。 - モジュールで宣言したクラスをオブジェクト経由で一度も読み取らない — 参照されない名前にスコープされるため、未使用の CSS として出力され、クラス名を検索しても見つかりません。
テーマ設定や上書きに支障をきたす問題:
- レイヤーで囲まれていないコンポーネントのスタイル — 優先順位が読み込み順によって決まり、アプリケーション側から上書きするには、次の上書きでさらに上回る必要がある詳細度のハックが必要になります。
@layer componentsで囲まれたアプリケーションのスタイル — ライブラリと同じ優先順位になるため、ライブラリを上書きできなくなります。それこそが、アプリケーションのスタイルの本来の役割です。- テーマ変更可能な色に Sass の色関数を使う(
darken($primary, 10%))— ビルド時に解決されるため、実行時にテーマを切り替えても結果に反映されません。color-mix()と相対色構文なら、動的に反映されます。 - モジュール内にテーマセレクターを置く(
.dark & { … })— コンポーネントを特定のテーマ方式に結び付け、さらにテーマを追加するたびに、それを含むすべてのコンポーネントに分岐が増えます。 - 色、余白、角丸の値をハードコードする — トークンを変更しても反映されず、監査でも検出されない未追跡の値になります。
予想外の動作:
- レイヤー化されていないスタイルは、読み込み順に関係なく、すべてのレイヤーに優先します。これはアプリケーション側の上書きが依存する仕組みであり、ライブラリのルールを誤ってレイヤー化しなかった場合に上書きしにくくなる理由でもあります。
- レイヤー内では通常どおり詳細度が適用されます。レイヤーが順位を決めるのは、レイヤー間 だけです。
@use "…" with (…)で設定したモジュールは、別のファイルから設定なしで読み込まれても、その設定を保持します。最初の読み込みが優先されます。_または-で始まるメンバーは、そのファイル内でのみ使用でき、@useや@forwardを通じて参照できません。math.$piなどの組み込みモジュールの変数は読み取り専用です。:global()は囲まれたセレクターのスコープを解除するため、モジュールの奥深くにある:globalブロックは、モジュールが防ぐはずの衝突をひそかに再び招きます。