見た目を検証するときに使っている非表示のブラウザペインで、測定値そのものが嘘になっていた、という記録です。
トップページの一番上、最初に目に入る大きな見せ場(ヒーロー)を、真っ暗な画面に問いが立ち上がって、そこからスクロールで写真が出て紙色に明ける、という作りに入れ替えた。
作り終えて実物を測ったら、上に重なっているナビゲーションの文字が墨色のままだった。ロゴが #302E2A、横のメニュー項目が #696560。背景は暗幕(#15130f)なので、コントラスト比はロゴ 1.37、メニュー項目 3.21。文字と背景の明暗差を 1〜21 で表す指標で、本文サイズの文字なら 4.5 以上が目安とされます。どちらも読めない。
暗幕でいる間だけナビを明るい側へ寄せる規則を入れてみた。ヒーロー側が <html> タグに目印(data-lp-hero="dark" という属性)を付けて、CSS がそれを見る形にした。
/* ヒーローが暗幕でいる間だけ立つ属性を見て、ナビを暗がりの側へ寄せる */
html[data-lp-hero='dark'] nav a, /* html に data-lp-hero="dark" が付いている間だけ当たる */
html[data-lp-hero='dark'] nav button,
html[data-lp-hero='dark'] nav span {
color: rgba(239, 228, 210, 0.8); /* 紙色。末尾の 0.8 は不透明度 */
}効かない。ただしロゴだけ効いている
測ると、ロゴの「綴Photo」は rgba(239, 228, 210, 0.8) に変わっていた。ところが横のメニュー項目(できること / Web版 / コンセプト)は rgb(105, 101, 96) のまま。どちらも nav の中の <a> で、同じ規則にかかるはずだった。
メニュー項目は Tailwind の text-lp-ink-light を持っている。ロゴは text-lp-ink。ここが違うので、ユーティリティとの優先順位で負けているのだろうと考えた。
!important を足した。
変わらない。
セレクタは当たっている
優先順位の話にする前に、そもそも規則が届いているかを確かめた。
const sel = "html[data-lp-hero='dark'] nav a";
document.querySelectorAll(sel).length; // このセレクタに当たる要素の数を数える → 5
link.matches(sel); // この link 自身が条件に合うか → true当たっている。次に、CSS ではなく要素に直接書いたらどうなるかを試した。
// CSS ファイルを経由せず、要素そのものに直接スタイルを書き込む(=最強の札)
link.setAttribute('style', 'color: rgb(4,5,6) !important');
link.getAttribute('style'); // ちゃんと書き込めているか確認 → "color: rgb(4,5,6) !important"
getComputedStyle(link).color; // その要素が実際に使っている色を読む → "rgb(105, 101, 96)"インラインの !important が効かない。ここで優先順位の説明は成り立たなくなった。これはサイトを書く側が出せる最強の札で、これに勝てる指定をこちらから書くことはできません。効かないなら、順位で負けているのではなく、そもそも別の何かが色を決めている。
手掛かりは最初の観察に戻ってあった。効いていたのはロゴだけで、ロゴのクラスは font-serif font-bold text-base text-lp-ink tracking-wider no-underline。メニュー項目は ... text-lp-ink-light hover:text-lp-ink transition-colors ...。
違いは text-lp-ink-light ではなく transition-colors のほうだった。
止まったタイムラインの上では、トランジションは 0% で固まる
このペインは document.visibilityState === 'hidden'(=画面に映っていない状態)で動いています。見えていないページを描き続けても意味がないので、ブラウザは描画に紐づく処理を止めます。前の作業で requestAnimationFrame — ブラウザが画面を描き替えるたびに呼んでくれる関数で、普通は1秒に60回くらい回ります — が1度も発火しないことは確認済みだった(500ms 待っても呼ばれた回数 0)。アニメーションやトランジションが「いま何秒経ったか」を数えるための時計(ドキュメントのタイムライン)も、同じ理由で進みません。
属性が立った瞬間に色の変化が起きて、transition-colors がトランジションを開始します。開始はします。ただしタイムラインが進みませんので、進行度は 0% のまま止まります。
測る前にトランジションを切る
測定手順を変えた。読む前に一時的なスタイルを差し込む。
// このペインは document timeline が止まっている=started した transition が 0% で固着する。
// transition を切って「最終値」を測る。
const probe = document.createElement('style'); // <style> タグを作る
probe.textContent = '*,*::before,*::after{transition:none !important}'; // 全要素(と ::before/::after の飾り)のトランジションを一律オフ
document.head.appendChild(probe); // head に差し込んで有効化
// ここで getComputedStyle を読む
probe.remove();差した状態で測り直したら、ナビの項目は全部 rgba(239, 228, 210, 0.8) になった。0.8 の透過なので暗幕と混ざって実効色は #C3BAAB、コントラスト比 9.66。紙色に明ければ元の #696560 に戻り、こちらは 5.35。どちらの局面でも読める。
そして !important は要らなかったので外した。効かないと思っていたのは測り方のほうで、規則は最初から正しく効いていた。危ないところで、本番の CSS に不要な !important を残すことになっていた。
学び
- 詳細度や
!importantが決めていたのはトランジションの行き先で、進行中の現在値はそれとは別に決まっていた。優先順位を足す方向で粘っていた時間が丸ごと無駄だった - インラインの
!importantが効かなかった時点で、優先順位の話は終わっていた。作者の!importantより上に置かれている宣言はトランジションだけ(CSS アニメーションは!importantに負ける側)。次に同じ壁に当たったら、トランジション・止まったタイムライン・そもそも別の要素を見ていないか、を順に見ます - 見た目を測る前に
* { transition: none !important }を差す、を手順に入れた。requestAnimationFrameが発火しないのと、トランジションが 0% で固着するのは同じ原因なので、このペインで色を読む限りは毎回必要になります(CSS アニメーションはこれでは切れないので、そこは別の手が要ります) - 犯人を
text-lp-ink-lightだと思ったのは、症状が「色」だったので同じ語彙を名乗っているクラスから疑ったからだった。真犯人のtransition-colorsは同じ行に並んでいたのに、色の話ではない飾りとして読み飛ばしていた - そしてこの間ずっと、疑う先はコードの中だけだった。壊れていたのはコードではなく測っている場所のほうで、規則は最初から効いていた
この記事は2026年7月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。