CSSで、画像の上に文字を読みやすく重ねる方法

​​CSS​HTML,Playwright,アクセシビリティ

画像の上に白い文字を重ねるときは、文字のある下のほうだけをグラデーションで暗くするか、文字の後ろに半透明の箱を敷くと、画像の明るい部分に文字がかかっても読めます。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つの箱に入れるだけです。

html
<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 でした)。

css
/* 画像と文字を、グリッドの同じマスに重ねる */
.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 が決めている測り方ではなく、比べるための目安です。

  1. 幅640pxの画面で、文字を白で描いた画面と、文字だけを透明にした画面を撮る(影や暗くした部分は残る)
  2. 2枚で色が違う画素を「文字の画素」とし、そこから2px以内にある、文字でない画素を「まわりの画素」とする
  3. まわりの画素ごとに、白とのコントラスト比を出す

見出しは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 だけでは、明るい部分で読めない

いちばん手軽なのは、文字に影を付ける方法です。

css
/* 影を重ねて、文字のまわりを暗くする */
.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段の比較。上の「何もしない」では、白い見出し「丘の上の小さな公園」と説明文のうち、左の暗い緑の丘にかかる部分は読めるが、右の明るい空と砂浜にかかる「さな公園」や説明文の後半は、背景にとけて見えにくい。下の「text-shadow を2つ重ねる」では、文字の輪郭が少し灰色になっているが、明るい部分の文字は上とほとんど変わらず見えにくい
影を重ねても(下)、明るい砂浜の上の文字は見えにくいまま

上の表のとおり、影を2つ重ねても、説明文のまわりの画素の39.1%は4.5:1に届きませんでした。影がない場合の40.5%から、ほとんど減っていません。見出しも、3:1に届かない画素が25.2%残りました。

画像全体を暗くすると、文字のない空まで暗くなる

文字の入った箱を画像と同じ大きさに広げ、半透明の黒で塗ると、画像全体が暗くなります。

css
/* 画像全体を暗くする */
.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)を足して、グラデーションが薄くなっていく場所を作ります。

css
/* 文字のある高さまでは暗さを保ち、その上で薄くする */
.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)
  );
}
上下2段の比較。上の「画像全体を暗くする(黒の60%)」では、文字は読めるが、空の水色や白い雲、太陽まで灰色にくすんでいる。下の「下のほうだけグラデーションで暗くする」では、空と雲と太陽は元の明るさのままで、見出しと説明文のある下のほうだけが暗くなり、白い文字が読める
画像全体を暗くした場合(上)と、下のほうだけを暗くした場合(下)

このグラデーションでは、見出しの最小は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色のグラデーションは、下から上へ一様に薄くなるので、説明文より上にある見出しのあたりでは、すでに薄くなっています。途中の色で文字のある高さまで暗さを保ち、そこから上で薄くすると、見出しの上のほうも暗くなりました。

文字の後ろに、半透明の箱を敷く

画像をなるべく暗くしたくないときは、文字の後ろにだけ半透明の箱を敷く方法もあります。

css
/* 文字の後ろに、半透明の箱を敷く */
.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 を付けています。

css
/* 比べる相手: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はみ出し、画像の上にある段落に重なりました。

上下2段の比較。上の「position: absolute で下に置く」では、白い見出し「丘の上の小さな公園と、海まで続く散歩道」が画像の上の端を越えて、画像の上にある黒い段落の文字に重なっている。下の「グリッドの同じマスに重ねる」では、画像の高さが文字に合わせて伸び、見出しと説明文がすべて画像の中に収まっていて、上の段落とも重ならない
幅320pxで文字が長いとき。absolute(上)は画像からはみ出し、グリッド(下)は画像が伸びる

グリッドで重ねた場合は、文字もマスの大きさに数えられるので、箱の高さは文字に合わせて301pxになりました。画像も高さ301pxに伸びましたが、object-fit: cover なので、縦横の比を保ったまま左右が切り取られ、引き伸ばされませんでした。画像の height: 100% を外すと、箱は301pxに伸びても、画像は180pxのままでした。

画像が読み込めないときのために、背景色を付けておく

画像が読み込めないと、白い文字の後ろはページの白い背景になります。存在しないファイルを src に指定し、文字には影を2つ重ねた .shadow-strong を付けて試しました。

上下2段の比較。上の「画像が読み込めない:背景色なし」では、白い背景の上で、白い見出しと説明文は灰色の影で輪郭が見えるが、文字の中は背景と同じ白で読みにくい。下の「画像が読み込めない:背景色あり」では、濃い灰色(青みがかった黒)の背景の上に、白い見出しと説明文がはっきり読める。どちらも左上に壊れた画像のアイコンと灰色の細い枠が出ている
画像が読み込めないとき。背景色がないと(上)、白い文字が白い背景の上で読みにくい

どちらも、箱は画像があるときと同じ高さ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からの対応です。

​Posted by 86