結論:CSSが効かない原因は3つしかない。!importantを足すのは最後で、まず「当たっているか→打ち消されていないか→上書き対象が同じか」を順に潰す
- ① そもそも当たっていない=セレクタのタイプミス・親子関係の誤り・要素が後からJSで差し替わっている。開発者ツールでその宣言が一覧に出てこないなら詳細度の話ですらない
- ② 打ち消されている=詳細度で負けている。数え方は「ID・クラス/属性/擬似クラス・要素/擬似要素」の3桁で、桁上がりはしない。クラスを11個重ねてもID1個に負ける(実測済み)
- ③ 上書き対象が違う=後から来たショートハンド(background・font・borderなど)が、先に書いた個別プロパティごと初期値に戻している
- @layer を使っているサイトでは詳細度より先にレイヤー順が効く。レイヤー外の宣言は常にレイヤー内より強く、!important を付けると順序がそっくり反転する(実測済み)
- 同点のときは「後に書いたほう」が勝つ。HTMLのclass属性の並び順は一切関係なく、効くのはCSSの記述順と<link>の読み込み順だけ
- !important はインラインstyle属性すら上書きする最強手だが、次に上書きしたい人が同じ手しか使えなくなる。使うのは他人のプラグインCSSに触れないときだけに限定する
指定したはずの色が変わらない。開発者ツールで見ると自分の書いた行に打ち消し線が入っている。仕方なく !important を付けたら今度はそれも効かない——。
CSSが効かないという症状は、実際には「当たっていない」「打ち消されている」「上書き対象が違う」の3つのどれかにほぼ必ず収まります。厄介なのは、この3つが見た目の症状ではまったく区別できないことです。どれも「書いたのに反映されない」としか見えません。だから当てずっぽうに !important を足すことになり、数か月後には!important だらけで誰も触れないCSSが残ります。
この記事では、切り分けの順番を固定したうえで、詳細度の数え方・:is() や :where() が詳細度を書き換える仕組み・@layer が詳細度より先に効く順序を、実際にChromium(141.0.7390.37 / ヘッドレス)で検証した結果つきで整理します。最後に、ある要素のあるプロパティについて「どのルールが勝っているか」を一覧で出すコンソールスニペットを置きます。これも実際に動かして出力を確認したものです。
まず切り分ける:効かないCSSは3つのどれか
開発者ツールの Elements(要素)パネルで、対象の要素を選び、右側の Styles に自分が書いた宣言が出てくるかどうかを見ます。ここで3方向に分かれます。
① 一覧に出てこない=セレクタが当たっていない
詳細度の問題ですらありません。セレクタのタイプミス、親子関係の勘違い(.card p と書いたが実際は .card > div > p で問題ない…ではなく、そもそも .card の外にある)、要素がJSで後から差し替えられていて別のクラスになっている、ビルド時にCSSファイルそのものが読み込まれていない、といった原因です。
確認は一瞬で終わります。コンソールで document.querySelectorAll('.your-selector').length を実行して、期待した数が返るかを見るだけです。0なら、そこから先の話はすべて無関係です。
② 出てくるが打ち消し線=詳細度で負けている
宣言は届いているが、より強いルールに負けています。これがこの記事の本題で、次の章から扱います。
③ 打ち消し線もないのに反映されない=ショートハンドに巻き込まれている
いちばん気づきにくいパターンです。先に background-color を書いたあとで、別のルールが background(ショートハンド)を指定していると、背景色は「負けた」のではなく「初期値に戻された」状態になります。ショートハンドは、自分が含む個別プロパティすべてを一度リセットしてから書き直すからです。詳しくは後半の章で実測値とともに扱います。
詳細度は3桁の数字。桁上がりはしない
詳細度(specificity)は (A, B, C) の3つの数字で表されます。
- A:IDセレクタの数(
#header) - B:クラス・属性セレクタ・擬似クラスの数(
.btn、[type="text"]、:hover) - C:要素セレクタ・擬似要素の数(
div、::before)
比較は左の桁から順に行い、上の桁で差がついた時点で決まります。ここが直感に反するところで、桁上がりは起きません。クラスを10個並べても11個並べても、ID 1個には絶対に勝てません。
実際に確かめました。次のCSSで、要素は <div id="t1" class="a b c d e f g h i j k"> です。
#t1 { color: rgb(1,0,0); } /* (1,0,0) */
.a.b.c.d.e.f.g.h.i.j.k { color: rgb(0,1,0); } /* (0,11,0) =後に書いてある */
getComputedStyle の結果は rgb(1, 0, 0)。クラス11個を後から書いてもID1個に負けます。「クラスをもう1個足して詳細度を上げる」という発想が、ID指定が絡んだ瞬間に無意味になるのはこのためです。
詳細度を上げずに勝つ方法=同点にして後に置く
同じ詳細度どうしの場合は後に書いたほうが勝ちます。つまり「相手と同じ形のセレクタを、読み込み順で後になる場所に書く」のがいちばん副作用が小さい勝ち方です。これも実測しました。
.t8a { color: rgb(8,0,0); }
.t8b { color: rgb(0,8,0); } /* 後に書いた */
要素側は <div class="t8b t8a">、つまりHTML上は t8b を先に書いています。結果は rgb(0, 8, 0) で .t8b の勝ち。class属性の並び順は詳細度にも優先順位にも一切影響しません。効くのはCSSファイル内の記述順と、<link> の読み込み順だけです。
:is() と :not() は詳細度を跳ね上げる。:where() はゼロにする
ここ数年でいちばん事故が増えたのがこの3つです。挙動が3者3様で、しかも見た目が似ています。
:is(A, B)… 引数の中でいちばん詳細度が高いものを採用する:not(A)… 同じく引数の詳細度を採用する(:not()自体は0):where(A, B)… 常に0。中に何を書いても詳細度に加算されない
:is() の落とし穴を実測しました。
:is(#nope, .ac) { color: rgb(1,0,0); } /* #nope は存在しないIDだが (1,0,0) になる */
.ac { color: rgb(0,1,0); } /* (0,1,0)。後に書いてある */
結果は rgb(1, 0, 0)。存在しないIDを引数に1つ混ぜただけで、そのセレクタ全体がID級の強さになります。「まとめて書けるから」と :is() に既存セレクタを放り込むと、あとから来るクラス指定が軒並み効かなくなるのはこれが原因です。
:not() も同じ性質です。
:not(.nope) { background-color: rgb(2,0,0); } /* (0,1,0) */
div { background-color: rgb(0,2,0); } /* (0,0,1)。後に書いてある */
結果は rgb(2, 0, 0)。:not(.nope) は「.nopeでない全要素」という非常にゆるい条件なのに、クラス1個ぶんの強さを持ったまま全要素に当たります。リセットCSSで :not() を多用すると後段が上書きしづらくなるのはこのためです。
逆に :where() は徹底してゼロです。
#t2 { color: rgb(0,0,1); }
:where(#t2#t2#t2) { color: rgb(9,9,9); } /* IDを3つ重ねても詳細度 0 */
結果は rgb(0, 0, 1)。IDを3つ重ねた :where() が、ID1個に負けます。ここから実務上の使い分けが決まります。「あとで必ず上書きされる前提の土台」=ベーススタイルやリセットは :where() で書く。そうすると、利用側はクラス1個で何の苦労もなく上書きできます。逆に絶対に上書きされたくない箇所で :is() を使うと、将来の自分が !important を使う羽目になります。
@layer は詳細度より「先に」効く
@layer(カスケードレイヤー)を使っているCSS——最近のフレームワークやデザインシステムでは一般的です——では、詳細度を比べる前にレイヤーの順序で勝負がつきます。ここを知らないと「詳細度では勝っているはずなのに効かない」という説明不能な現象に見えます。
ルールは2つです。
- レイヤー外(どの
@layerにも入っていない宣言)が、すべてのレイヤーより強い - レイヤー同士では、
@layer base, theme;のように宣言した順が優先順位で、後のレイヤーほど強い
実測しました。
@layer base, theme;
@layer theme { #t5 { border-top-color: rgb(5,0,0); } } /* (1,0,0) だがレイヤー内 */
@layer base { #t5 { border-top-color: rgb(0,5,0); } } /* (1,0,0) だがレイヤー内 */
.t5c { border-top-color: rgb(0,0,5); } /* (0,1,0) =レイヤー外 */
結果は rgb(0, 0, 5)。詳細度で圧倒的に劣るクラス指定が、レイヤー内のID指定2本に勝ちます。レイヤーを採用しているサイトで自分のCSSを効かせたいなら、詳細度を上げるのではなくレイヤーの外に置くのが正解です。
!important を付けると、レイヤーの順序がそっくり反転する
ここが最大の罠です。!important が付いた宣言どうしの比較では、レイヤーの優先順位が逆転します。「先に宣言したレイヤーのほうが強い」「レイヤー外がいちばん弱い」に変わります。
@layer base { #t6 { border-top-color: rgb(6,0,0) !important; } }
@layer theme { #t6 { border-top-color: rgb(0,6,0) !important; } }
#t6 { border-top-color: rgb(0,0,6) !important; } /* レイヤー外 */
結果は rgb(6, 0, 0)。通常なら最強のはずのレイヤー外の !important が最弱になり、いちばん先に宣言した base レイヤーが勝ちます。これは仕様どおりの挙動で、「土台側が本当に譲れない値を死守できるように」という設計意図によるものですが、知らずに踏むと !important を足すほど負けるという意味不明な状況になります。
!important を足す前にやる3手
!important はインラインの style 属性すら上書きします。これも実測しました。<div id="t7" style="color: rgb(0,7,0)"> に対して外部CSSで #t7 { color: rgb(7,0,0) !important; } を当てると、結果は rgb(7, 0, 0)。CSSファイル側の勝ちです。
つまり !important は確かに効きます。効きすぎるから問題なのです。一度使うと、次にその値を変えたい人(多くの場合は半年後の自分)は同じ !important を使うしか手がなくなり、以降は「より後に書いた !important 」の消耗戦になります。使う前に、上から順に3手を試してください。
- 第1手:同じ詳細度で、後に置く。 相手と同じ形のセレクタを、読み込み順で後になるCSSに書く。副作用ゼロで、いちばん素直に勝てます
- 第2手:セレクタを1段だけ具体的にする。
.btnが効かないなら.card .btnや.btn.btn(同じクラスを2回重ねる書き方は合法で、詳細度だけを +1 できます)にする。ID指定に手を出すのは最後です - 第3手:相手の土台を
:where()に書き換える。 自分が管理しているCSS(自社テーマ・子テーマ)が相手なら、上書きされる側を:where()で包むだけで、以降すべての上書きが楽になります
この3手が全部使えない場合——つまり相手が他社プラグインのCSSで、読み込み順も変えられないときだけが !important の出番です。その際は、なぜ必要だったかを1行コメントで必ず残してください。
同点なら「後勝ち」。@media は詳細度を上げない
詳細度もレイヤーも同じなら、最後は記述順です。ここで誤解されやすいのが @media です。
/* b.css を先に、a.css を後に読み込む */
/* b.css */ .x { color: rgb(3,3,3); }
/* a.css */ .x { color: rgb(1,1,1); }
@media (min-width:1px) { .x { color: rgb(2,2,2); } }
結果は rgb(2, 2, 2)。ここから2つ確認できます。後に読み込んだ a.css が、先に読んだ b.css に同一詳細度で勝つこと。そして @media は詳細度を1ミリも上げず、単に「その位置に書かれた宣言」として順番だけで評価されることです。
実務での意味は明確です。メディアクエリのブロックは、対応する通常指定より必ず後ろに書く。SP用スタイルをファイル先頭にまとめ、PC用の共通指定を下に置くと、同じ詳細度のまま後勝ちで上書きされ、「スマホだけ効かない」が発生します。CSS設計の話ではなく、単純な順序の問題です。
ショートハンドは「負ける」のではなく「消す」
冒頭に挙げた③のパターンです。
.cc1 { background-color: rgb(3,0,0); }
.cc2 { background: rgb(0,3,0); } /* 後に書いたショートハンド */
要素は <div class="cc1 cc2">。結果は rgb(0, 3, 0) です。これは「詳細度で負けた」のではなく、後から来た background が、background-color を含む配下のプロパティを全部いったん初期値に戻してから塗り直した結果です。
同じ事故が font・border・flex・grid・inset でも起きます。特に危険なのが border で、border-bottom: none を効かせたい箇所の後ろで誰かが border: 1px solid #ccc を書くと、丁寧に消した1辺が復活します。「打ち消し線が付いていないのに効いていない」ときは、ショートハンドを疑う——これを切り分けの3本目に入れておいてください。
WordPressで「子テーマのCSSが効かない」典型パターン
ここまでは純粋なCSSの話ですが、WordPressサイトでは読み込み順そのものが自分の手を離れているため、別の切り分けが要ります。頻度の高い順に3つです。
① 子テーマのCSSが親テーマより先に読まれている
wp_enqueue_style() で子テーマのCSSを登録するとき、第3引数(依存)に親テーマのハンドルを渡していないと、読み込み順が保証されません。同一詳細度なら先に読まれたほうが負けるので、子テーマ側が丸ごと無効に見えます。
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style( 'parent-style', get_template_directory_uri() . '/style.css' );
wp_enqueue_style(
'child-style',
get_stylesheet_directory_uri() . '/style.css',
array( 'parent-style' ), // ← ここが重要。親の後ろに並ぶ
wp_get_theme()->get( 'Version' ) // ← キャッシュ対策のバージョン
);
} );
② プラグインのCSSが自分より後に読まれている
フォーム系・スライダー系のプラグインは、自分のテーマCSSより後ろに出力されることがよくあります。この場合は詳細度を上げるより、該当CSSの登録ハンドルを調べて、自分のCSSの依存に加えるほうが確実です。ハンドル名はページのソースで id="xxxx-css" の形で確認できます(-css を除いた部分がハンドル名です)。
③ そもそも古いCSSが配信されている
「何をやっても変わらない」ときの最終確認です。キャッシュ系プラグインやCDNが旧ファイルを返していないか、style.css?ver= のバージョン番号が更新されているかを見ます。開発者ツールのNetworkタブでCSSファイルを開き、自分が今書いた行が本当に含まれているかを目視するのが確実です。詳細度を疑う前にこれをやると、無駄な30分が消えます。
勝っているルールを1発で出すコンソールスニペット
開発者ツールのStylesパネルは優秀ですが、レイヤー名や、複数ファイルにまたがる同じプロパティの並びを俯瞰するには向きません。次のスニペットは、指定した要素の指定したプロパティについて、マッチしている全ルールを「レイヤー名・セレクタ・値・!important・ファイル名」で一覧化します。Chromium 141 で出力を確認済みです。
// 使い方: 開発者ツールで対象要素を選択($0 になる)→ 下を貼って実行
(function (prop, el) {
el = el || $0;
var rows = [];
var walk = function (rules, layer) {
for (var i = 0; i < rules.length; i++) {
var r = rules[i];
if (r instanceof CSSStyleRule) {
var hit = false;
try { hit = el.matches(r.selectorText); } catch (e) {}
var v = r.style.getPropertyValue(prop);
if (hit && v) rows.push({
layer: layer || '(レイヤー外)',
selector: r.selectorText,
value: v,
important: r.style.getPropertyPriority(prop) === 'important',
from: (r.parentStyleSheet && r.parentStyleSheet.href)
? r.parentStyleSheet.href.split('/').pop() : '<style>'
});
} else if (r.cssRules) {
walk(r.cssRules,
(typeof CSSLayerBlockRule !== 'undefined' && r instanceof CSSLayerBlockRule)
? (r.name || '(匿名レイヤー)') : layer);
}
}
};
for (var s = 0; s < document.styleSheets.length; s++) {
try { walk(document.styleSheets[s].cssRules, null); }
catch (e) { rows.push({ layer: '-', selector: '(別ドメインCSSで中身が読めない)',
value: '-', important: false,
from: document.styleSheets[s].href }); }
}
if (el.style.getPropertyValue(prop)) rows.push({
layer: '-', selector: 'style属性(インライン)',
value: el.style.getPropertyValue(prop),
important: el.style.getPropertyPriority(prop) === 'important', from: 'HTML'
});
console.table(rows);
console.log('最終的に採用された値:', getComputedStyle(el).getPropertyValue(prop));
})('color', $0); // ← 第1引数に調べたいプロパティ名
先ほどの @layer + !important のテストページで実行すると、次の3行が出て、最終値が rgb(6, 0, 0)(base レイヤー)であることが一目で分かります。
layer selector value important
base #t6 rgb(6, 0, 0) true
theme #t6 rgb(0, 6, 0) true
(レイヤー外) #t6 rgb(0, 0, 6) true
実務で効くのは3列目と5列目です。自分が知らない !important がどのファイルから来ているのかが即座に分かります。なお、別ドメインから読み込んでいるCSS(CDN上のフォントやプラグイン)は cssRules にアクセスできず例外になるため、その旨を1行として出すようにしてあります。これが出た場合は、そのファイルだけは開発者ツールのStylesパネルで確認してください。
公開前チェックリスト
- 効かないCSSに遭遇したら、まず
document.querySelectorAll()で当たっているかどうかを先に確定させる - 打ち消し線があるなら詳細度。クラスを足して勝とうとする前に、ID指定が相手かどうかを見る(相手にIDがあるなら何個足しても無駄)
- 打ち消し線が無いのに効いていないなら、後続のショートハンド(
background/font/border/flex)を疑う :is()と:not()は引数の詳細度を持ち込む。存在しないIDを混ぜるだけで全体がID級になる- あとから上書きされる前提の土台は
:where()で書く。上書きの手間を将来に押し付けない @layerがあるサイトではレイヤー外に置くのが最短。!importantはレイヤー順を反転させるので足すほど負けることがある- メディアクエリは対応する通常指定より必ず後ろに書く(
@mediaは詳細度を上げない) - WordPressでは
wp_enqueue_style()の依存指定で読み込み順を固定する。効かない原因がキャッシュだった、を最初に潰す !importantは最後の手段。使ったら理由を1行コメントで残す
関連記事(コーディング実務Tips)
- z-indexが効かない本当の理由と直し方|詳細度では勝っているのに重なり順が変わらないときは、スタッキングコンテキストが原因です
- position:fixedが効かない・一緒にスクロールしてしまう原因と直し方|宣言は届いているのに効かない代表例。祖先の
transformが基準を奪っています - object-fitとaspect-ratioが効かない・画像の縦横比が崩れる原因と直し方|こちらは「箱の高さが決まっていない」という前提条件の問題です
- flexアイテムが縮まない・ellipsisが効かない原因はmin-width:auto|初期値が効いているだけで、上書きの問題ではないパターン
- CSS Gridがはみ出す・1frの列が縮まない原因と直し方|詳細度の問題ではなく、frトラックの最小値がautoであることが原因のパターン
- SVGの色が変わらない・サイズが効かない原因10パターン|詳細度0のプレゼンテーション属性が継承には勝つ=SVGだけで起きるカスケードの例外
- scroll-behavior: smoothが効かない原因10パターン|スムーススクロールが滑らない/滑りすぎる原因の実測版
- gapが効かない・隙間が2倍になる原因9パターン
- transformが効かない・効きすぎる原因
- CSSで中央寄せできない原因は「誰が中心を決めているか」
- 背景画像が表示されない原因はurl()の基準と箱の有無
- メディアクエリが効かない原因はviewportと単位
- inputやselectのCSSが効かない原因はappearance
- 画像の下の4pxの隙間とinline-blockの謎のすき間
- ボタンが押せない・リンクがクリックできない原因と直し方
- height: 100% が効かないのはなぜか
- ::before / ::after が表示されない原因と直し方
- 日本語の改行がおかしい・URLがはみ出す原因
- marginが効かない・余白が消える原因はマージンの相殺
- text-overflow: ellipsisが効かない原因と直し方
- transitionが効かない原因と直し方
- CSS変数(カスタムプロパティ)が効かない9つの原因と直し方
- nth-childが効かない原因9パターン|nth-childが効かないときの切り分け(要素の種類を見ていない/数に入るもの)
CSSが効かないという相談の多くは、実のところ設計の問題が先送りされた結果として現れます。!important が5つを超えたあたりから修正コストは加速度的に上がるので、そこが土台を :where() やレイヤーで整理し直す分岐点です。
多言語・ベトナム語サイトの制作でお困りではありませんか?
フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。
多言語サイト制作の詳細を見る →すぐに相談したい方は お問い合わせフォームへ