写真の上でヘッダーメニューが読めない問題を mix-blend-mode: difference で解決する
タイトル案
- 写真の上でヘッダーが読めない!を CSS 1 行で解決する
mix-blend-mode: differenceposition: fixedのヘッダーでmix-blend-modeが効かないときの対処法- 背景の明暗に合わせて文字色が反転するヘッダーの作り方
ファーストビューに人物写真を敷いたサイトで、こんな経験はないでしょうか。
- ヘッダーメニューが髪の暗い部分に重なって、黒文字だと読めない
- かといって白文字にすると、今度は明るい背景の部分で消える
写真は左右で明るさがバラバラだし、ウィンドウ幅が変われば重なる位置も変わります。「ここは白、ここは黒」と決め打ちできないのが厄介なところです。
今回は、これを mix-blend-mode: difference で解決した話と、途中でハマった position: fixed の落とし穴をまとめます。
やりたいこと
- 暗い背景(髪など)の上 → 文字を明るく
- 明るい背景の上 → 文字を暗く
- ロゴ(例では
ÉTOFFE)は常にブランドカラーのまま、反転させない
最初に考えた方法と、その限界
まず思いつくのはこのあたりです。
- 画像を見て色を決め打ちする … レスポンシブで破綻する
- 写真の領域だけ色を変える(左は濃色・右は白) … レイアウトが固定なら有効
- テキストを 2 枚重ねて
clip-pathで切り分ける … 厳密だけど、髪の輪郭に合わせるのは現実的じゃない
どれも「写真のどこが暗いか」を人間が管理する必要があります。そこを CSS に丸投げできないか、というのが出発点でした。
解決策:mix-blend-mode: difference
difference は、下にある色との差分を表示する合成モードです。白文字に指定すると、こうなります。
| 下の色 | 白文字 + difference の見え方 |
|---|---|
| 黒・暗い髪 | 明るく(白っぽく)見える |
| 白・明るい地 | 暗く(黒っぽく)見える |
白(#fff)との差分を取るので、実質的に背景色の反転になります。結果として、背景が暗ければ明るく、明るければ暗く、自動でコントラストが付くわけです。
.nav {
color: #fff;
mix-blend-mode: difference;
}
……が、これをヘッダーにそのまま入れても効きません。ここからが本題です。
ハマりどころ:position: fixed の内側では効かない
ヘッダーはだいたいこうなっているはずです。
.header {
position: fixed;
inset: 0 0 auto;
z-index: 900;
}
この状態で .header の子要素である .nav に mix-blend-mode を付けても、下のヒーロー写真とは合成されません。見た目は「ずっと白のまま」「何も変わらない」になります。
理由は、position: fixed + z-index がスタックコンテキストを作るからです。mix-blend-mode は、自分が属するスタックコンテキストの中にある背景としか合成されません。ヘッダーの内側から見ると、写真は「外の世界」なので届かない、というわけです。
対処:ブレンドは fixed している要素そのものに付ける
.header {
position: fixed;
inset: 0 0 auto;
z-index: 900;
}
/* トップの FV に重なっているときだけ反転 */
.header.is-over-photo {
color: #fff;
mix-blend-mode: difference;
}
ヘッダー自身に付ければ、合成相手はページ全体(ルート)になるので、下の写真とちゃんと混ざります。
ほかにも効かなくなるケース
同じ理屈で、祖先要素に以下があると合成が閉じ込められます。効かないときはここを疑ってください。
transform/filter/backdrop-filter/perspectiveopacity: 1 未満will-change: transformなどisolation: isolate(これは意図的に隔離するためのプロパティ)contain: paint
あと基本的なところですが、ヘッダーに不透明な背景色が付いていると意味がありません。ブレンドさせたい状態では background: transparent にしておきます。
ロゴだけ反転させたくないとき
mix-blend-mode をヘッダー全体に付けると、中のロゴも一緒に反転します。ブランドカラーを守りたいなら、ロゴをブレンド対象の外に出すのが確実です。
<!-- ロゴは fixed ヘッダーの外に置く -->
<a class="logo" href="/">ÉTOFFE</a>
<header class="header is-over-photo">
<span class="logo-spacer" aria-hidden="true"></span>
<nav class="nav">...</nav>
<button class="hamburger" type="button">...</button>
</header>
.logo {
position: fixed;
top: 0;
left: 0;
z-index: 901; /* ヘッダーより手前 */
color: #1e2642; /* ブランドの濃色。反転させない */
}
.header.is-over-photo {
color: #fff;
mix-blend-mode: difference;
}
/* レイアウト用のダミー。実体のロゴは外にある */
.logo-spacer {
visibility: hidden;
pointer-events: none;
inline-size: 160px; /* ロゴと同じ幅を確保 */
}
DOM が分かれるぶん、幅の同期が必要になるのが唯一の手間です。ロゴを画像にしている場合は、.logo-spacer にも同じ画像を visibility: hidden で置いてしまうと、幅がズレません。
WordPress などで組むなら、「トップページのときだけ is-over-photo(または data-theme="inverse")を付ける」という運用にしておくと楽です。
スクロールしたら通常のヘッダーに戻す
半透明の白背景に切り替わるタイプなら、スクロール後はブレンドを切って通常色に戻します。
.header.is-scrolled {
color: #1e2642;
background: rgb(255 255 255 / 70%);
backdrop-filter: blur(10px);
mix-blend-mode: normal;
}
backdrop-filter はスタックコンテキストを作りますが、この状態ではもうブレンドしていないので問題ありません。逆に言うと、backdrop-filter と mix-blend-mode: difference は同時に使えないと覚えておくと迷いません。
クラスの付け外しは IntersectionObserver が手軽です。
const header = document.querySelector('.header');
const hero = document.querySelector('.hero');
if (header && hero) {
const io = new IntersectionObserver(
([entry]) => {
header.classList.toggle('is-over-photo', entry.isIntersecting);
header.classList.toggle('is-scrolled', !entry.isIntersecting);
},
{ rootMargin: '-80px 0px 0px 0px' } // ヘッダーの高さぶん
);
io.observe(hero);
}
注意点
導入前に知っておきたい弱点もあります。
- 中間色の上では消える。 グレー(
#808080)と白の差分はほぼ#7f7f7f。つまり背景と同化します。肌・服・空など中間の明るさが広い写真では、色が濁ったり読みにくくなったりします。 - ブランドカラーは再現できない。 出るのは「背景の反転色」なので、狙った
#1e2642にはなりません。白寄り/黒寄りのどちらかになります。 - コントラストは保証されない。 アクセシビリティ的に十分かどうかは写真次第なので、実機での確認は必須です。写真側に薄いグラデーションのオーバーレイを重ねて、明暗のレンジを狭めておくと安定します。
- ロゴを除外するなら DOM が分かれる。
古い環境向けのフォールバックを入れるなら、こんな形になります。
@supports not (mix-blend-mode: difference) {
.header.is-over-photo {
color: #fff;
text-shadow: 0 1px 3px rgb(0 0 0 / 60%);
}
}
使い分け
| 方法 | 向いているケース |
|---|---|
mix-blend-mode: difference | 写真の上で、とにかく手軽に読みやすくしたい |
| 領域で色分け(左は濃色・右は白) | 「左コピー/右写真」でレイアウトが安定している |
テキスト 2 枚 + clip-path | ピクセル単位で色を厳密に固定したい |
| 写真にオーバーレイを重ねる | 文字色を 1 色に固定したい/可読性を最優先したい |
「かんたんに読みやすく」なら difference、「デザインの色を厳密に」なら領域分けや clip-path、という住み分けです。
まとめ
- 背景の明暗に合わせて文字色を変えたいときは
mix-blend-mode: difference - ヘッダーが
position: fixedなら、ブレンドは子要素ではなく fixed 要素自体に付ける - 効かないときは、祖先の
transform/filter/opacity/isolationを疑う - ロゴを守りたいなら、ロゴはブレンドするヘッダーの外に出す
- 中間色の上では効果が薄いので、実際の写真で必ず確認する
CSS 数行で「写真に合わせて文字が読める」状態が作れるので、ヒーロー画像を使うサイトでは覚えておいて損のないテクニックです。
コメント