注意
本記事はAIの支援を受けて執筆しています。
目次
- 1. 問題:なぜスタイルが「効かない」のか?
- 2. 原因:ブラウザがスタイルを決める6つの基準
- 3. Origin(スタイルの出所)― 最も重要な優先順位
- 4. Cascade Layers(@layer)― 2022年からの新ルール
- 5. Specificity(詳細度)― 数値の計算アルゴリズム
- 6. !important ― 最後の手段として使うべき機能
- 7. インラインスタイル ― 特殊な優先順位を持つ存在
- 8. 実践例(React + TypeScript)
- 9. 落とし穴(Pitfall)
- 10. まとめ
- 11. 参考資料
- 次回予告
1. 問題:なぜスタイルが「効かない」のか?
ある日、次のようなコードを書きました。
<div class="card card--featured">
<button class="btn">購入する</button>
</div>
/* ボタンの基本スタイル */
.btn { background: blue; }
/* 特別なカードの中のボタン */
.card--featured .btn { background: red; }
/* 後から上書きしようとした */
.btn { background: green; }
さて、ボタンの背景色は何色になるでしょうか?
答えは 赤 です。なぜなら .card--featured .btn は詳細度(specificity) 0-2-0 で、.btn は 0-1-0 だからです。後に書いた .btn は詳細度が低いため、勝てません。
さらに厄介な例:
/* グローバルスタイル */
body .header .nav .link {
color: blue;
}
/* コンポーネントスタイル */
.nav-link {
color: red;
}
半年後、誰も覚えていない長いセレクタチェーンがプロジェクト中に散らばり、新しいデザイントークンを導入しようとしても、どこから手をつければいいのか分かりません。
なぜこうなるのか?
多くの開発者はCSSを「装飾言語」と誤解しています。しかし実際にはCSSは、複雑な優先順位アルゴリズムで動作する スタイルの優先順位を決定する仕組み です。そのルールを理解しなければ、「魔法のように効かない」という謎から永遠に抜け出せません。
2. 原因:ブラウザがスタイルを決める6つの基準
ブラウザは以下の基準を順に比較し、最終的なスタイルを決定します。
CSS Cascade Level 6 についての補足
Level 6 からは、詳細度と Order of Appearance の間に @scope による Scoping Proximity が追加されます。しかし現時点では実務での利用例がまだ少ないため、本記事では割愛します。別の記事で解説予定です。
仕様通りの優先順位(CSS Cascade Level 5 & 6):
この記事では、現実的によく使われる 1,2,3,5 に焦点を当てます。
3. Origin(スタイルの出所)― 最も重要な優先順位
スタイルは3つの「出所(origin)」のいずれかに属します。
解説:
-
User-Agent Origin: ブラウザのデフォルトスタイル(例:
<h1>のデフォルトフォントサイズ) - User Origin: ユーザーがブラウザ設定などで指定したスタイル
- Author Origin: 開発者が書いたCSS。通常は最も高い優先度を持つ
ただし !important が付くと優先順位が逆転します。
- 通常宣言: Author > User > User-Agent
- Important宣言: User-Agent > User > Author
実務上の注意: 日常的な開発では、ほぼすべてのCSS競合は Author Origin 内で発生します。User-Agent の !important に出会うことはほとんどありません(アクセシビリティ関連の特殊スタイルに限られます)。
4. Cascade Layers(@layer)― 2022年からの新ルール
@layer は2022年以降すべてのモダンブラウザでサポートされ、CSS設計の考え方を変えました。
なぜ必要なのか?
従来、スタイルを上書きするには詳細度を上げるために長いセレクタチェーンを書くか、やむを得ず !important を使うしかありませんでした。その結果、プロジェクトは「追加専用」の複雑なコードへと変貌していました。
@layer を使うと、詳細度に関係なく、レイヤーの順序だけで優先順位を制御できます。
重要な補足: 異なるレイヤーに属する宣言同士の場合、ブラウザは詳細度よりも先に Layer のステップで決定します。
また、!important が付くとレイヤーの優先順位は 完全に逆転 します。
| 宣言の種類 | 優先順位(高い → 低い) |
|---|---|
| 通常宣言(!important なし) | レイヤーなし → 最後のレイヤー → ... → 最初のレイヤー |
| Important 宣言(!important あり) | 最初のレイヤー → ... → 最後のレイヤー → レイヤーなし |
実践的なレイヤー構造:
/* 1. レイヤーの順序を最初に定義 */
@layer reset, tokens, base, components, utilities, overrides;
/* 2. 各レイヤーにスタイルを割り当て */
@layer reset {
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer tokens {
:root { --color-primary: #3498db; --space-md: 1rem; }
}
@layer base {
body { font-family: system-ui, sans-serif; }
h1 { font-size: 2rem; }
}
@layer components {
.btn { background: var(--color-primary); padding: var(--space-md); }
.card { border-radius: 0.5rem; overflow: hidden; }
}
@layer utilities {
.hidden { display: none; }
}
@layer overrides {
/* 一時的な修正や実験的なスタイル */
}
この構造なら、次のようなことが可能になります:
/* 非常に高い詳細度を持つルールでも... */
.reset-layer #main .container .card.card--featured button.btn.primary {
color: blue;
}
/* ...このシンプルなユーティリティクラスが勝つ(utilities は components より後だから)*/
@layer utilities {
.text-red { color: red; } /* 詳細度 0-1-0 でも勝つ! */
}
補足: @layer は現代のプロジェクトで推奨されるCSS組織化の方法として徐々に普及しています。
5. Specificity(詳細度)― 数値の計算アルゴリズム
詳細度は、同じOrigin・同じLayer内での競合を解決するために使われます。3つの列で計算します。
| 列 | 含まれるもの | 例 |
|---|---|---|
| ID | IDセレクタ | #header |
| CLASS | クラスセレクタ, 属性セレクタ, 擬似クラス |
.card, [type="text"], :hover
|
| TYPE | 要素セレクタ, 擬似要素 |
div, p, ::before
|
比較方法: ID列 → CLASS列 → TYPE列 の順に比較します(桁上がりのような足し算はしません)。
/* 例1: #header が勝つ(ID列で比較) */
#header { color: red; } /* 1-0-0 */
.header-large { color: blue; } /* 0-1-0 */
/* 結果: 赤 */
/* 例2: CLASS列で比較 */
.card .btn { color: red; } /* 0-2-0 */
.btn.large { color: blue; } /* 0-2-0(同点 → 後から書いた方が勝つ) */
/* 結果: 後から書いた方の色 */
/* 例3: :is() と :where() の違い */
:is(#header, .nav) a { color: red; } /* 詳細度 1-0-1(最も高い引数を採用) */
:where(#header, .nav) a { color: blue; } /* 詳細度 0-0-1(:where() は無視) */
/* 結果: 赤 */
特殊なケース:
-
*(ユニバーサルセレクタ)は詳細度にカウントされません(0-0-0) -
:where()とその引数は詳細度にカウントされません -
:is()は最も高い詳細度を持つ引数を採用します - 擬似要素
::before,::afterは TYPE列(0-0-1) -
:not()も引数の詳細度を採用します。例::not(.active)の詳細度は0-1-0。これは面接でよく聞かれるポイントです。
6. !important ― 最後の手段として使うべき機能
!important は Origin の優先順位を逆転させ(3節)、さらに Cascade Layers の順序も逆転させます(4節)。
同じOrigin内での例:
.btn-custom {
background: red !important;
}
/* 詳細度を上げても !important なしでは上書きできない */
.special .btn-custom {
background: blue; /* まだ赤 */
}
/* 再び !important を使い、詳細度も同じか高くする必要がある */
.special .btn-custom {
background: blue !important; /* やっと上書き */
}
なぜ危険か?
一度 !important を使い始めると、それを上書きするためにさらに !important が必要になり、プロジェクト全体が「詳細度の債務(specificity debt)」に陥ります。
実際に !important を使ってもよいケース:
- サードパーティ製ウィジェットのスタイルをどうしても上書きしたい場合(最終手段)
- ユーティリティクラス(例:
.hidden { display: none !important; })— ただし、できれば@layer utilitiesで管理する方が望ましい
重要な注意点:
-
!importantは@keyframes内部では無効です -
Transition 中の値は、たとえ
!importantよりも優先されます(ただし transition が終われば通常のスタイルに戻ります)
7. インラインスタイル ― 特殊な優先順位を持つ存在
React における真のインラインスタイルは次のように書きます。
<button style={{ color: 'red' }}>Click</button>
インラインスタイルの優先順位: これは通常のID/CLASS/TYPEの3列には当てはまりません。解説の現場ではよく 1-0-0-0(4列目)と表現されますが、これはあくまで覚えやすくするための慣習であり、CSS仕様上の正式な詳細度の値ではありません。正確には、インラインスタイルは「スタイル属性(style attribute)」としてカスケードの別の層で処理されます。
.btn {
color: blue; /* 詳細度 0-1-0 → インラインスタイルには勝てない */
}
実際には、インラインスタイルは以下のものにのみ上書きされます。
- 同じ Author Origin 内の
!important宣言 - アニメーションやトランジションによって生成される値(実行中のみ)
注意: よくある誤解
-
EmotionのCSS prop:
<div css={{ color: 'red' }} />はクラス(.css-xxxxx)を生成するだけで、インラインスタイルではありません。詳細度は0-1-0です。 - Reactの
style={{ ... }}だけが本当のインラインスタイルです。
結論: 動的な値が必要な場合に限りインラインスタイルを使いましょう。乱用するとCSSのキャッシュや保守性が損なわれます。Emotion/Styled Componentsを使っているなら、それはクラスベースのスタイルであって、インラインスタイルではありません。
8. 実践例(React + TypeScript)
例1: 高い詳細度が上書きを難しくする問題
ComponentA.module.css
.button {
background-color: #3498db;
padding: 12px 24px;
}
/* 詳細度が非常に高い: 3クラス → 0-3-0 */
.page .container .button {
background-color: #e74c3c;
}
Button.tsx
import React from 'react';
import styles from './ComponentA.module.css';
export const Button: React.FC<React.ButtonHTMLAttributes<HTMLButtonElement>> = ({
children,
className,
...props
}) => {
return (
<button
className={`${styles.button} ${className || ''}`}
{...props}
>
{children}
</button>
);
};
Page.tsx – 上書きしようとする
import React from 'react';
import { Button } from './Button';
import pageStyles from './Page.module.css';
export const ProblemPage = () => {
return (
<div className={pageStyles.page}>
<div className={pageStyles.container}>
<Button className={pageStyles.customButton}>
購入する
</Button>
</div>
</div>
);
};
/* Page.module.css – 詳細度が低いため上書きできない */
.customButton {
background-color: #2ecc71; /* 0-1-0 では 0-3-0 に勝てない */
}
実務上の補足: この例は詳細度の問題を理解しやすくするために単純化しています。実際のCSS Modulesではクラスハッシュ(.ComponentA_button__abc など)が生成されますが、高い詳細度による問題は以下のような場面で同様に発生します。
- グローバルCSS
- CSS-in-JS(Styled Components, Emotion)
- CSS Modules内の
:global() - デザインシステムのCSS
したがって、詳細度を理解し制御することは依然として重要です。
正しい対処法: コンポーネント内では詳細度をフラットに保ち(クラス1つだけ)、長いネストを避ける。または @layer を使って詳細度に頼らず優先順位を制御する。
例2: Cascade Layers で解決する
styles/layers.css – プロジェクトのエントリーポイント
@layer reset, tokens, base, components, utilities;
@layer components {
.btn {
background-color: #3498db;
padding: 12px 24px;
}
/* 詳細度が高くても utilities レイヤーが後なら上書きされる */
.card .btn {
background-color: #e74c3c;
}
}
@layer utilities {
.bg-green {
background-color: #2ecc71;
}
}
// ButtonV2.tsx
import './styles/layers.css';
export const ButtonV2 = ({ children, className }) => (
<button className={`btn ${className}`}>{children}</button>
);
// PageV2.tsx
<ButtonV2 className="bg-green">購入する</ButtonV2> // 詳細度に関係なく緑になる
例3: Stylelint で詳細度をチームで管理する
本番プロジェクトでは Stylelint を使うのが現実的です。
npm install stylelint stylelint-config-standard
.stylelintrc.json
{
"extends": ["stylelint-config-standard"],
"rules": {
"selector-max-specificity": ["1,3,2", {
"ignoreSelectors": [":global", ":local", "::placeholder"]
}],
"selector-max-id": 1,
"selector-max-class": 3,
"selector-max-type": 2
}
}
※ 1,3,2 はあくまで一例であり、CSSの標準ルールではありません。プロジェクトの規模やチームの合意に応じて調整してください。
CI への組み込み:
// package.json
{
"scripts": {
"lint:css": "stylelint \"src/**/*.css\""
}
}
9. 落とし穴(Pitfall)
1. 詳細度の計算を間違えない
正しい方法: 列ごとに比較する。桁上がりのような足し算はしない。
2. :not() の詳細度
:not() の詳細度はゼロではない。
:not(.active) /* 詳細度 0-1-0 */
div:not(#header) /* 詳細度 1-0-1(#header が内部にあるため) */
3. Transition の優先順位(正確な説明)
Transition が実行中の間、その値は すべての !important よりも優先されます。Transition 終了後は通常のスタイルに戻ります。
4. 真のインラインスタイルと Emotion の違い
// 真のインラインスタイル – 詳細度の3列には当てはまらず、覚えやすさから 1-0-0-0 と表現される
<div style={{ color: 'red' }} />
// EmotionのCSS prop – クラスを生成、詳細度 0-1-0
<div css={{ color: 'red' }} />
5. @scope(CSS Cascade Level 6)― 別記事で解説予定
@scope (.card) {
/* .card の範囲内のみに適用 */
.btn { color: red; }
}
Scoping proximity はソース順よりも優先されることがありますが、現時点ではまだ一般的ではありません。
10. まとめ
重要なポイント
-
CSSは「装飾言語」ではなく「スタイルの優先順位を決定する仕組み」 です。ルールを理解すれば「なぜ効かないのか」が解決します。
-
優先順位の正しい順序: Origin+Importance → Layers → 詳細度 → Scoping Proximity → ソース順。
-
!importantは最後の手段。詳細度の債務を生みます。本当に必要な場面(ユーティリティ.hiddenなど)以外は使わない。 -
@layerは現代のプロジェクトで推奨される方法。詳細度を上げなくても上書きを制御できます。 -
インラインスタイルは非常に高い優先順位を持つ(慣習的に
1-0-0-0と表現されるが、仕様上の正式な詳細度ではない)。動的な値に限定して使い、乱用しない。また、Emotionのcssprop はインラインスタイルではない。 -
Stylelint で詳細度を管理 する。自分で関数を書く必要はない。ただし
!importantを完全に禁止するのは現実的ではないので、コードレビューでコントロールする。 -
@scopeと Scoping Proximity は近い将来の注目トピック。
今日から使えるチェックリスト
-
Stylelint に
selector-max-specificityルールを設定しましたか? -
!importantを使っている箇所はありますか?@layer utilitiesに置き換えられませんか? -
1-3-2(ID:1, クラス:3, 要素:2)を超えるセレクタはありませんか? -
@layerを明確に定義しましたか? -
React で
style={{}}のインラインスタイルを乱用していませんか? -
Emotionの
cssprop をインラインスタイルと誤解していませんか? -
:is(),:where(),:not()の詳細度の挙動を理解していますか? -
@scopeについて一度調べてみましたか?(将来のために)
11. 参考資料
- MDN: Cascade
- MDN: Specificity
- MDN: @layer
- MDN: !important
- CSS Cascade Level 5 Spec
- CSS Cascade Level 6 Spec (Working Draft)
- Stylelint: selector-max-specificity
- Specificity of :not()
次回予告
👉 【Frontend CSS – パート3】なぜ font-family は継承されるのに background は継承されないのか?
