CSSで、画像の上に文字を読みやすく重ねる方法
画像の上に白い文字を重ねるときは、文字のある下のほうだけをグラデーションで暗くするか、文字の後ろに半透明の箱を敷くと、画像の明るい部分に文字がかかっても読めます。text-shadow だけでは、明るい部分で文字がほとんど見えないままでした。画像と文字を、position: absolute で置くのではなく、グリッドの同じマスに重ねると、スマホの幅で文字が長くなっても画像からはみ出しません。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。画像は、明るい空や砂浜と暗い丘が文字の後ろに混ざるように、筆者が SVG で描いた風景を使いました。写真ではないので、数値はこの画像での結果です。HTML と CSS のページを Playwright で開き、画面を撮って、文字のすぐまわりの画素とのコントラスト比を計算しました。Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げは試していません。
画像と文字を、グリッドの同じマスに重ねる
HTML は、画像と文字を1つの箱に入れるだけです。
<div class="hero">
<img src="photo.png" alt="">
<div class="hero-text scrim-hold">
<h2>丘の上の小さな公園</h2>
<p>季節の花と、遠くまで見渡せる散歩道を紹介します。</p>
</div>
</div>
CSS では、箱をグリッドにして、中の画像と文字の両方に grid-area: 1 / 1 を指定します。どちらも1行目・1列目のマスに置かれます。検証では、HTML で後に書いた文字が画像の上に重なりました(見出しのある位置で、いちばん手前にある要素を調べると h2 でした)。
/* 画像と文字を、グリッドの同じマスに重ねる */
.hero {
display: grid;
color: #fff;
background-color: #1f2a30; /* 画像が出ないときも文字が読める色 */
}
.hero > * {
grid-area: 1 / 1;
}
.hero img {
display: block;
width: 100%;
height: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.hero-text {
align-self: end;
margin: 0;
padding: 16px 20px;
}
grid-area に 1 / 1 と書くと、行と列の始まりの線がどちらも1番目になります。文字は align-self: end で、マスの下に寄せています。画像の object-fit: cover は、マスの形が画像の縦横の比と合わないときに、画像を引き伸ばさずに切り取って埋める指定です(object-fit の説明)。後の「スマホの幅で文字が長くなると、absolute では画像からはみ出す」の節で書くとおり、文字が長くなったときに効いてきます。
この記事の画像は飾りなので、alt="" にしています。<img> の説明のとおり、空の alt は、その画像がコンテンツの重要な部分ではないことを示します。画像そのものに伝えたい内容があるときは、alt に書きます。
読みやすさを、文字のすぐまわりとのコントラスト比で比べる
読みやすさの目安には、WCAG 2.2 の達成基準 1.4.3 コントラスト(最低限)を使いました。文字と背景のコントラスト比は4.5:1以上、大きな文字(18ポイント以上、または14ポイント以上の太字)なら3:1以上が求められています。コントラスト比は、2つの色の相対輝度 L1、L2(明るいほうが L1)から (L1 + 0.05) / (L2 + 0.05) で出します。白の文字なら、背景が明るいほど比は1:1に近づきます。
画像の上では、背景の色が場所ごとに変わります。そこで筆者は、次の方法で測りました。WCAG が決めている測り方ではなく、比べるための目安です。
- 幅640pxの画面で、文字を白で描いた画面と、文字だけを透明にした画面を撮る(影や暗くした部分は残る)
- 2枚で色が違う画素を「文字の画素」とし、そこから2px以内にある、文字でない画素を「まわりの画素」とする
- まわりの画素ごとに、白とのコントラスト比を出す
見出しは36px(約27ポイント)なので大きな文字として3:1、説明文は16pxなので4.5:1を基準にしました。結果は次のとおりです。最小は、まわりの画素の中でいちばん低かった比です。
| 重ね方 | 見出しの最小 | 見出しで3:1未満の画素 | 説明文の最小 | 説明文で4.5:1未満の画素 |
|---|---|---|---|---|
| 何もしない | 1.13 | 32.2% | 1.10 | 40.5% |
text-shadow を1つ |
1.15 | 29.0% | 1.11 | 39.6% |
text-shadow を2つ重ねる |
1.30 | 25.2% | 1.26 | 39.1% |
| 画像全体を黒の45%で暗くする | 3.72 | 0% | 3.66 | 39.0% |
| 画像全体を黒の60%で暗くする | 6.21 | 0% | 6.18 | 0% |
| 下からのグラデーション(2色) | 2.77 | 4.8% | 5.41 | 0% |
| 下からのグラデーション(暗さを保つ) | 5.80 | 0% | 7.92 | 0% |
| 半透明の箱を敷く | 6.33 | 0% | 6.18 | 0% |
text-shadow だけでは、明るい部分で読めない
いちばん手軽なのは、文字に影を付ける方法です。
/* 影を重ねて、文字のまわりを暗くする */
.shadow-strong {
text-shadow:
0 0 2px rgb(0 0 0 / 0.8),
0 0 8px rgb(0 0 0 / 0.6);
}
text-shadow は、カンマで区切って影をいくつも重ねられます。ぼかしの小さい濃い影と、ぼかしの大きい薄い影を重ねました。

上の表のとおり、影を2つ重ねても、説明文のまわりの画素の39.1%は4.5:1に届きませんでした。影がない場合の40.5%から、ほとんど減っていません。見出しも、3:1に届かない画素が25.2%残りました。
画像全体を暗くすると、文字のない空まで暗くなる
文字の入った箱を画像と同じ大きさに広げ、半透明の黒で塗ると、画像全体が暗くなります。
/* 画像全体を暗くする */
.dim-strong {
align-self: stretch;
display: flex;
flex-direction: column;
justify-content: flex-end;
background: rgb(0 0 0 / 0.6);
}
黒の45%(rgb(0 0 0 / 0.45))では、見出しは3:1を超えましたが、説明文は最小3.66で、4.5:1に届かない画素が39.0%ありました。黒の60%にすると、説明文も最小6.18になりました。
ただ、画像の上から40%の範囲(空と雲のあたり)の平均の相対輝度は、何もしないときの0.662から、黒の60%では0.090まで下がりました。次の節の画像の上段のとおり、文字のない空や雲まで灰色にくすみます。
文字の下だけをグラデーションで暗くする
文字の箱に、下から上へ透明になっていくグラデーションの背景を付けると、暗くなるのは文字のあるあたりだけです。箱の上側に余白(padding-top)を足して、グラデーションが薄くなっていく場所を作ります。
/* 文字のある高さまでは暗さを保ち、その上で薄くする */
.scrim-hold {
padding-top: 64px;
background: linear-gradient(
to top,
rgb(0 0 0 / 0.75),
rgb(0 0 0 / 0.55) 60%,
rgb(0 0 0 / 0)
);
}

このグラデーションでは、見出しの最小は5.80、説明文の最小は7.92で、どちらも基準を下回る画素はありませんでした。画像の上から40%の平均の相対輝度は0.662で、何もしないときと同じです。
途中の色(rgb(0 0 0 / 0.55) 60%)を入れずに、linear-gradient(to top, rgb(0 0 0 / 0.75), rgb(0 0 0 / 0)) の2色だけにすると、説明文は最小5.41で基準を超えましたが、見出しは最小2.77で、3:1未満の画素が4.8%ありました。2色のグラデーションは、下から上へ一様に薄くなるので、説明文より上にある見出しのあたりでは、すでに薄くなっています。途中の色で文字のある高さまで暗さを保ち、そこから上で薄くすると、見出しの上のほうも暗くなりました。
文字の後ろに、半透明の箱を敷く
画像をなるべく暗くしたくないときは、文字の後ろにだけ半透明の箱を敷く方法もあります。
/* 文字の後ろに、半透明の箱を敷く */
.box {
justify-self: start;
margin: 16px;
border-radius: 6px;
background: rgb(0 0 0 / 0.6);
}
justify-self: start を付けると、幅640pxの画像の上で、箱の幅は文字に合わせた422pxになりました。付けないと、箱は608px(左右の margin を除いた幅いっぱい)に広がりました。見出しの最小は6.33、説明文の最小は6.18でした。暗くなるのは箱の中だけで、画像の上から40%の平均の相対輝度は、何もしないときと同じ0.662でした。

スマホの幅で文字が長くなると、absolute では画像からはみ出す
画像と文字を重ねるときは、箱に position: relative、文字に position: absolute; bottom: 0 を指定する書き方もあります。この書き方とグリッドで重ねる書き方を、幅320pxの画面で、長い見出しと説明文を入れて比べました。文字には、どちらも前の節の .scrim-hold を付けています。
/* 比べる相手:position: absolute で文字を下に置く */
.hero-abs {
position: relative;
color: #fff;
background-color: #1f2a30;
}
.hero-abs img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.hero-abs .hero-text {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
position: absolute の要素は通常のフローから外れ、レイアウトの中にその要素のための場所が作られません。検証でも、文字は箱の高さの計算に入らず、箱の高さは画像の16:9のまま180pxで、文字の箱は高さ301pxでした。文字は下にそろえてあるので、上へ121pxはみ出し、画像の上にある段落に重なりました。

グリッドで重ねた場合は、文字もマスの大きさに数えられるので、箱の高さは文字に合わせて301pxになりました。画像も高さ301pxに伸びましたが、object-fit: cover なので、縦横の比を保ったまま左右が切り取られ、引き伸ばされませんでした。画像の height: 100% を外すと、箱は301pxに伸びても、画像は180pxのままでした。
画像が読み込めないときのために、背景色を付けておく
画像が読み込めないと、白い文字の後ろはページの白い背景になります。存在しないファイルを src に指定し、文字には影を2つ重ねた .shadow-strong を付けて試しました。

どちらも、箱は画像があるときと同じ高さ360pxでした。画像の aspect-ratio を外すと、箱は文字の高さと同じ108pxに縮みました。また、alt="" にしていても、Chromium では左上に壊れた画像のアイコンと灰色の枠が出ました。.hero に背景色を付けていないと、説明文の箱の右端あたり(文字の後ろの背景)の画素は白(rgb(255, 255, 255))で、白い文字とのコントラスト比は1.00でした。.hero に background-color: #1f2a30 を付けておくと、その画素は rgb(31, 42, 48) になり、コントラスト比は14.66でした。最初の CSS で .hero に背景色を書いているのは、このためです。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で、この記事で使っている機能を調べました。
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
display: grid |
57 | 52 | 10.1 |
グリッドでの align-self |
57 | 52 | 10.1 |
グリッドでの justify-self |
57 | 45 | 10.1 |
grid-area |
57 | 52 | 10.1 |
aspect-ratio |
88 | 89 | 15 |
object-fit |
32 | 36 | 10 |
text-shadow |
2 | 3.5 | 1.1 |
linear-gradient() |
26 | 16 | 7 |
rgb(0 0 0 / 0.6) のような空白区切りの書き方 |
65 | 52 | 12.1 |
いちばん新しいのは aspect-ratio で、Safari は15からの対応です。





