CSSで、マウスを乗せた白黒の画像をカラーに戻す方法
白黒の画像を、マウスを乗せたときだけカラーに戻すには、画像に filter: grayscale(100%) を付けて白黒にし、:hover のときに grayscale(0%) へ戻します。transition を付ければ、色は少しずつ戻ります。画像のファイルは1枚のままで、白黒の画像を別に用意する必要はありません。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開いてマウスとキーボードで操作し、getComputedStyle() の値と、スクリーンショットの画素の色を記録しました。画像は、検証のために描いた SVG を使っています。タッチ操作の端末は、Playwright の Pixel 7 のエミュレーションで試しました。Firefox と Safari、スマートフォンの実機は試していません。

GIF の上部の文字、マウスの絵、画像の下の grayscale の値は、動きを見やすくするために描き足したものです。
filter: grayscale() で白黒にし、:hover で 0% に戻す
画像をリンクの a で包みます。
<a class="photo" href="#1">
<img src="photo.svg" alt="丘の上の猫と家" width="400" height="300">
</a>
/* 画像をふだんは白黒にし、マウスを乗せるとカラーに戻す */
.photo {
display: block;
width: 240px;
}
.photo img {
display: block;
width: 100%;
height: auto;
filter: grayscale(100%); /* 白黒にする */
transition: filter 400ms ease;
}
.photo:hover img,
.photo:focus-visible img {
filter: grayscale(0%); /* カラーに戻す */
}
filter は、要素の見た目にぼかしや色の変化などの効果をかけるプロパティです。grayscale() は、filter に指定できる関数のうち、白黒にするものです。100% で完全に白黒になり、0% では元の色のままです。100% の代わりに 1、0% の代わりに 0 と書いても同じです。

Chromium で画像にマウスを乗せ、filter の計算値と、画像の左上に置いた4色の四角の色を読み取りました。
| 乗せる前 | 乗せて 600ms 後 | 外して 600ms 後 | |
|---|---|---|---|
filter の計算値 |
grayscale(1) |
grayscale(0) |
grayscale(1) |
赤 #ff0000 の四角 |
(54, 54, 54) | (255, 0, 0) | (54, 54, 54) |
緑 #00ff00 の四角 |
(182, 182, 182) | (0, 255, 0) | (182, 182, 182) |
青 #0000ff の四角 |
(18, 18, 18) | (0, 0, 255) | (18, 18, 18) |
黄 #ffff00 の四角 |
(237, 237, 237) | (255, 255, 0) | (237, 237, 237) |
白黒のときは、どの色も赤・緑・青が同じ値の灰色になりました。ただ、明るさは色ごとに違い、同じ最大の明るさの赤・緑・青でも、緑は明るい灰色、青はほとんど黒になりました。Filter Effects の仕様では、grayscale(100%) の灰色を、赤に 0.2126、緑に 0.7152、青に 0.0722 を掛けて足した値で求めます。255 にそれぞれを掛けると 54、182、18 になり、測った値と合います。
マウスを外すと、:hover が当てはまらなくなり、画像は白黒に戻りました。
CSS では、:hover と並べて :focus-visible にも同じ filter を指定しています。ページを開いて Tab キーを押すと、リンクに :focus-visible が当てはまり、filter は grayscale(0) になりました。キーボードで操作する人にも、どの画像を選んでいるかが色で伝わります。
transition で、色が少しずつ戻る
transition: filter 400ms ease を付けているので、色は 400ms かけて戻ります。マウスを乗せた直後に transition を一時停止し、100ms ずつ進めて値を読み取りました。
| 経過時間 | 0ms | 100ms | 200ms | 300ms | 400ms |
|---|---|---|---|---|---|
grayscale() の値 |
1 | 0.591 | 0.198 | 0.040 | 0 |
| 赤の四角の色 | (54, 54, 54) | (136, 32, 32) | (215, 11, 11) | (247, 2, 2) | (255, 0, 0) |

transition で 400ms かけて変化させたところ、はじめの 200ms で値は 0.2 まで下がり、残りの 200ms でゆっくり 0 に近づきました。200ms の時点で、色の大部分が戻っています。
戻す側を grayscale(0%) ではなく filter: none と書いても、同じ動きになりました。途中の値は grayscale(0.591489)、grayscale(0.197597) と、grayscale(0%) と書いたときとまったく同じで、最後だけ計算値が none になります。仕様のフィルターの補間の規則では、片方が none のときは、もう片方と同じ関数の並びを、効果のない値で置いて補間します。
ほかのフィルターと組み合わせるときは、関数の並びをそろえる
白黒にするだけでなく少し暗くもしたい場合は、filter に関数を並べて書きます。このとき、:hover の側にも同じ関数を同じ順番で書きます。
/* 白黒にして、少し暗くする */
.photo img {
filter: grayscale(100%) brightness(0.8);
}
/* 並びをそろえて、両方を元に戻す */
.photo:hover img {
filter: grayscale(0%) brightness(1);
}
この書き方では、マウスを乗せると transition が始まり、100ms 後の値は grayscale(0.591489) brightness(0.881702) でした。2つの関数が、それぞれ少しずつ元の値に近づきます。
一方、:hover の側を filter: brightness(1) だけにしたときは、transition が始まりませんでした。マウスを乗せた直後に getAnimations() で画像のアニメーションを調べると空でした。filter の計算値はその瞬間から brightness(1) になり、画像は一度にカラーになりました。
:hover の filter |
transition | 100ms 後の filter |
|---|---|---|
grayscale(0%) brightness(1) |
始まる | grayscale(0.591489) brightness(0.881702) |
brightness(1) |
始まらない | brightness(1)(一度に切り替わる) |
仕様では、関数の並びが対応していないフィルターは、途中の値を作らずに切り替える(離散的に補間する)と決められています。MDN の filter の補間の説明にも、同じことが書かれています。途中の値を作れない(離散的に補間される)値どうしの transition は、transition-behavior が初期値の normal のときは始まりません。
transition で色を戻すときは、ふだんと :hover で、filter の関数を同じ順番で並べます。効果をなくしたい関数は、消さずに grayscale(0%) や brightness(1) のような、効果のない値にします。
マウスのない端末では、はじめからカラーで見せる
スマートフォンのようにマウスのない端末では、画像の上にポインターを置いたままにできません。何もしないと、画像は白黒で表示され、タップしたときだけカラーになります。そこで、hover メディア特性を使い、乗せる操作が簡単にできない端末では、はじめからカラーにします。
/* マウスのように乗せられる操作がない端末では、はじめからカラーで見せる */
@media (hover: none) {
.photo img {
filter: none;
}
}
Playwright の Pixel 7 のエミュレーションでページを開くと、(hover: none) が当てはまり、画像の filter は最初から none でした。マウスの環境では (hover: hover) が当てはまり、これまでどおり grayscale(1) です。
この指定がないときも、Pixel 7 のエミュレーションで試しました。
| Pixel 7 のエミュレーション | @media (hover: none) あり |
なし |
|---|---|---|
| 開いた直後 | none(カラー) |
grayscale(1)(白黒) |
| 画像をタップしたあと | none(カラー) |
grayscale(0)(カラー) |
| 画像の外をタップしたあと | none(カラー) |
grayscale(1)(白黒) |
指定がないと、画像をタップしたときだけ :hover が当てはまってカラーになり、ほかの場所をタップすると白黒に戻りました。この検証では、タップしてもリンク先へ移らないように、クリックの動作をスクリプトで止めています。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で、この記事で使っている機能を調べました。
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
filter(接頭辞なし) |
53 | 35 | 9.1 |
@media (hover) |
38 | 64 | 9 |
:focus-visible |
86 | 85 | 15.4 |
互換性データの注記によると、Chrome 40 以前では hover の判定に不具合があり、マウスやトラックパッドを使うパソコンでも (hover: none) が当てはまっていました。





