画像をホバーしたときに、JavaScriptを使わずCSSだけで別の画像へ切り替える方法を解説します。
結論から書くと、切り替え自体は :hover で background-image を差し替えるだけです。ふわっと切り替えたい場合は、2枚重ねて opacity を動かします。
なお、2026年10月に測り直したところ、Chrome と Safari では transition: background-image がそのままクロスフェードするようになっていました。以前この記事に書いていた「background-image にトランジションは効かない」は、現在は正しくありません。詳しくは後述します。
▼やりたいことと使う方法
| やりたいこと | 方法 |
|---|---|
| パッと切り替える | :hover で background-image を差し替える |
| ふわっと切り替える(全ブラウザ) | 2枚重ねて opacity をトランジションする |
| スライド・拡大・回転などで見せる | 2枚重ねて transform や clip-path を動かす |
| リクエスト数を減らす | スプライト画像と background-position |
<img>タグで切り替える |
srcは変えられないので2枚とも置いて重ねる |
| 解像度で出し分ける | image-set() か <picture> |
スポンサーリンク
画像切り替えの基本
結論:通常時とホバー時の2枚を用意し、background-image を :hover で差し替えます。
画像を <img> ではなく背景画像として指定するのがポイントです。<img> の src はCSSから変更できないため、CSSだけで完結させるなら背景画像を使います。
CSSの基本設定
まず、通常時の状態を作ります。
.image-container {
width: 200px;
height: 150px;
background-image: url('通常の画像のURL');
background-size: cover;
background-position: center;
}
background-size と background-position も指定しておいてください。省くと、画像の縦横比が要素と違うときに切れ方が意図せず変わります。切り抜かれ方の考え方はobject-fitとは?画像の幅100%表示とトリミングのCSSにまとめています。
ホバー時の画像切り替え
.image-container:hover {
background-image: url('ホバーした状態の画像のURL');
}
これで切り替わります。ただしホバーした瞬間に画像の読み込みが始まるため、初回だけ一瞬何も表示されないことがあります。
手元のChromeで通信を記録して確かめました。この書き方だと、ホバー用の画像が取得されるのは最初にマウスを乗せた瞬間です。あとで説明する2枚重ねにすると、ページを開いた時点で2枚とも取得されます。
background-image のトランジションは、いまは Chrome と Safari で効く
結論:2026年10月時点では、Chrome と Safari は background-image の切り替えをクロスフェードします。Firefox は補間しません。
この記事では以前「background-image は離散値なのでトランジションが効かない」と書いていました。これは現在の挙動としては誤りだったので訂正します。
1秒かけて画像を切り替えながら、計算後の値を1フレームずつ記録した結果がこちらです。
- 確認した環境:macOS 26.5.1 の Chrome 154 / Firefox 157 / WebKit 27.2、iPhone 16 Pro Max のシミュレータ(iOS 18.2)のSafari。2026年10月9日に確認
▼transition: background-image 1s を指定したときの挙動
| ブラウザ | 途中の計算値 | 見え方 |
|---|---|---|
| Chrome 154 | cross-fade(...) の比率が変化 |
クロスフェードする |
| Safari(iOS 18.2) | cross-fade(...) の比率が変化 |
クロスフェードする |
| Firefox 157 | 変化せず、すぐ切り替え後の値 | 一瞬で切り替わる |
Safari では、6秒のトランジションの途中で画面を撮ると2枚の画像が重なって透けて見えました。本当に混ざっています。
/* Chrome と Safari ではクロスフェードする。Firefox では一瞬で切り替わる */
.image-container {
background-image: url('通常の画像のURL');
transition: background-image 0.3s ease;
}
.image-container:hover {
background-image: url('ホバーした状態の画像のURL');
}
Firefox では transition-behavior を足しても混ざらない
Firefox で transition-behavior: allow-discrete を足すと、トランジションが「動く」ようにはなります。ただし混ざるわけではなく、指定した時間のちょうど半分の時点で、画像が一気に入れ替わります。
/* Firefox では 0.5秒の時点でパッと入れ替わる(フェードはしない) */
.image-container {
transition: background-image 1s;
transition-behavior: allow-discrete;
}
1秒のうち0.5秒までは元の画像のまま、そこから先は切り替え後の画像のまま、という動きでした。フェードさせたいなら、やはり2枚重ねです。
スポンサーリンク
2枚重ねが今も基本である3つの理由
結論:Chrome と Safari がクロスフェードするようになった今でも、重ねる方式を基本にしています。
- Firefox でフェードしない。見え方をそろえたいなら重ねるしかない
- ホバー用の画像が先に読み込まれる。初回ホバーで一瞬欠ける問題が起きない
- フェード以外の見せ方ができる。スライド・拡大・回転はクロスフェードでは作れない
2枚重ねてopacityで切り替える
擬似要素にホバー時の画像を持たせ、その opacity をトランジションします。
<div class="image-container"></div>
.image-container {
position: relative;
width: 200px;
height: 150px;
background-image: url('通常の画像のURL');
background-size: cover;
}
.image-container::after {
content: "";
position: absolute;
inset: 0;
background-image: url('ホバーした状態の画像のURL');
background-size: cover;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-container:hover::after {
opacity: 1;
}
inset: 0 は top/right/bottom/left をまとめて0にする指定です。これで擬似要素が親要素にぴったり重なります。
動きを減らす設定にしている人への配慮も入れておきます。
@media (prefers-reduced-motion: reduce) {
.image-container::after {
transition: none;
}
}
切り替えの見せ方7パターン
結論:2枚重ねを土台にすれば、フェード以外の見せ方も同じ考え方で作れます。動かす対象を opacity から transform や filter に変えるだけです。
ここから紹介する7つは、すべて Chrome 154・Firefox 157・WebKit 27.2 で表示を撮って、意図どおりに切り替わることを確認しています。
1. フェード
上で紹介した opacity を動かす方法です。いちばん無難で、どの画像でも破綻しません。
2. スライド
ホバー時の画像を左から滑り込ませます。overflow: hidden は必須です。これを外して試したところ、ホバーしていない状態でも待機中の画像が要素の左側にはみ出して見えてしまいました。
.slide {
position: relative;
overflow: hidden;
width: 200px;
height: 150px;
background: url('通常の画像のURL') center / cover;
}
.slide::after {
content: "";
position: absolute;
inset: 0;
background: url('ホバーした状態の画像のURL') center / cover;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.slide:hover::after {
transform: translateX(0);
}
3. 拡大
画像を1枚だけ使い、ホバーで内側の画像を拡大します。親要素の overflow: hidden を忘れると、拡大した画像が要素の外まではみ出します。これも外して確認しました。
.zoom {
overflow: hidden;
width: 200px;
height: 150px;
}
.zoom img {
display: block;
width: 100%;
height: 150px;
object-fit: cover;
transition: transform 0.3s ease;
}
.zoom:hover img {
transform: scale(1.15);
}
4. 回転(カードのように裏返す)
表と裏に別々の画像を置いて、Y軸で180度回します。backface-visibility: hidden は、裏を向いた面を描かないようにする指定です。
<div class="flip">
<div class="flip-inner">
<div class="face front"></div>
<div class="face back"></div>
</div>
</div>
.flip {
width: 200px;
height: 150px;
perspective: 600px;
}
.flip-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.5s;
transform-style: preserve-3d;
}
.flip:hover .flip-inner {
transform: rotateY(180deg);
}
.face {
position: absolute;
inset: 0;
backface-visibility: hidden;
background-size: cover;
background-position: center;
}
.front { background-image: url('通常の画像のURL'); }
.back { background-image: url('ホバーした状態の画像のURL'); transform: rotateY(180deg); }
手元のChrome 154では、backface-visibility を visible にしても回転の途中・完了後ともに見え方は変わりませんでした。ただし面の重なり順をブラウザ任せにすることになるので、意図を明示する意味で hidden を書いています。
5. グレースケールを解除する
画像は1枚で済みます。通常時をモノクロにしておき、ホバーで色を戻します。実績ロゴの一覧などで使いやすい見せ方です。
.gray img {
display: block;
width: 100%;
height: 150px;
object-fit: cover;
filter: grayscale(1);
transition: filter 0.3s ease;
}
.gray:hover img {
filter: grayscale(0);
}
6. 暗くして文字を出す
画像は変えず、半透明の黒と文字を重ねます。リンク先が分かるので、カード型のリンクではこれがいちばん親切です。
<a class="overlay" href="/example/"><span>詳しく見る</span></a>
.overlay {
position: relative;
display: grid;
place-items: center;
width: 200px;
height: 150px;
background: url('通常の画像のURL') center / cover;
}
.overlay::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
opacity: 0;
transition: opacity 0.3s;
}
.overlay span {
position: relative; /* 黒い帯より手前に出す */
color: #fff;
font-weight: 700;
opacity: 0;
transform: translateY(8px);
transition: opacity 0.3s, transform 0.3s;
}
.overlay:hover::before,
.overlay:hover span {
opacity: 1;
}
.overlay:hover span {
transform: translateY(0);
}
7. 円形に広げる(clip-path)
ホバー時の画像を、中心から円で広げて見せます。clip-path はトランジションできるので、形を変えながら切り替えられます。
.circle {
position: relative;
width: 200px;
height: 150px;
background: url('通常の画像のURL') center / cover;
}
.circle::after {
content: "";
position: absolute;
inset: 0;
background: url('ホバーした状態の画像のURL') center / cover;
clip-path: circle(0% at 50% 50%);
transition: clip-path 0.4s ease;
}
.circle:hover::after {
clip-path: circle(75% at 50% 50%);
}
スポンサーリンク
スプライト画像を使用する
結論:2枚を1枚の画像にまとめ、background-position をずらして切り替えます。リクエスト数が半分になり、切り替えも一瞬です。
.image-container {
width: 200px;
height: 150px;
background-image: url('スプライト画像のURL');
background-position: 0 0; /* 通常の画像の位置 */
}
.image-container:hover {
background-position: 0 -150px; /* ホバーした状態の画像の位置 */
}
background-position はアニメーションできるので、こちらはトランジションが効きます。
.image-container {
transition: background-position 0.3s ease;
}
ただし位置が動くアニメーションになるので、フェードとは見え方が違います。切り替えというより「スライドする」印象になる点は意識して使ってください。
なお、HTTP/2以降は複数ファイルの並行取得が効率化されているため、リクエスト数を減らす目的だけでスプライトを使う必要性は以前より下がっています。管理の手間と相談して決めてください。
imgタグの画像を切り替えたい場合
結論:<img> の src はCSSから変更できません。画像として意味を持たせたいなら、2枚とも配置して opacity で切り替えます。
<div class="img-swap">
<img src="通常の画像.jpg" alt="商品の外観">
<img src="ホバー時の画像.jpg" alt="" aria-hidden="true">
</div>
.img-swap {
position: relative;
display: inline-block;
}
.img-swap img {
display: block;
width: 100%;
}
.img-swap img:last-child {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.3s ease;
}
.img-swap:hover img:last-child {
opacity: 1;
}
2枚目の alt は空にして aria-hidden="true" を付けてください。同じものを写した装飾用の画像なので、スクリーンリーダーが2回読み上げる必要はありません。
背景画像との使い分けは次のとおりです。
▼背景画像とimgタグの使い分け
| 背景画像 | <img>タグ |
|
|---|---|---|
| 画像の意味 | 装飾 | コンテンツ |
| 代替テキスト | 付けられない | altで付けられる |
| 検索エンジン | 画像として拾われにくい | 拾われる |
| 向いている用途 | 飾りのアイコン・背景 | 商品写真・記事の図 |
商品写真のように内容として意味のある画像は <img> を使ってください。背景画像にすると alt が付けられず、画像検索にも出なくなります。
pictureとimage-set()を使うとき
結論:どちらも「ホバーで切り替える」ための機能ではなく、「同じ画像を環境に合わせて出し分ける」ための機能です。切り替えと組み合わせるときは、重ね方式の上に乗せます。
picture は src を差し替えられないので、2枚とも置く
<picture> の中身(どの <source> を採用するか)はブラウザが読み込み時に決めるもので、ホバーで切り替えることはできません。<picture> ごと2つ置いて重ねます。
<div class="pic-swap">
<picture>
<source srcset="通常.webp" type="image/webp">
<img src="通常.jpg" alt="商品の外観">
</picture>
<picture class="top">
<source srcset="ホバー.webp" type="image/webp">
<img src="ホバー.jpg" alt="" aria-hidden="true">
</picture>
</div>
.pic-swap {
position: relative;
width: 200px;
height: 150px;
}
.pic-swap img {
display: block;
width: 200px;
height: 150px;
object-fit: cover;
}
.pic-swap .top {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.3s;
}
.pic-swap:hover .top {
opacity: 1;
}
WebPやAVIFで出し分けるかどうかは、容量と画質を実際に比べてから決めてください。同じ画質に揃えて測った結果はJPEG・WebP・AVIFを同じ画質に揃えて実測したにまとめています。
背景画像で解像度を出し分けるなら image-set()
背景画像には srcset が使えません。代わりに image-set() を使います。Chrome 154・Firefox 157・WebKit 27.2・Safari(iOS 18.2)のすべてで CSS.supports() が true を返しました。表示は Chrome・Firefox・WebKit で確認しています。
.image-container {
background-image: image-set(
url('通常の画像@1x.png') 1x,
url('通常の画像@2x.png') 2x
);
background-size: cover;
}
.image-container::after {
content: "";
position: absolute;
inset: 0;
background-image: image-set(
url('ホバー画像@1x.png') 1x,
url('ホバー画像@2x.png') 2x
);
background-size: cover;
opacity: 0;
transition: opacity 0.3s;
}
.image-container:hover::after {
opacity: 1;
}
古い環境も拾うなら、-webkit-image-set() と、どちらも解釈できないブラウザ向けの url() を手前に書いておきます。
.image-container {
background-image: url('通常の画像@1x.png'); /* 解釈できないブラウザ向け */
background-image: -webkit-image-set(url('通常の画像@1x.png') 1x, url('通常の画像@2x.png') 2x);
background-image: image-set(url('通常の画像@1x.png') 1x, url('通常の画像@2x.png') 2x);
}
ホバーできない端末での代替
結論:スマートフォンにはホバーがありません。ホバーでしか見えない情報は、タッチ端末では見られないと考えてください。
iOS 18.2 の Safari で判定を取ったところ、(hover: hover) も (pointer: fine) も false でした。つまり次のメディアクエリの中身は、iPhoneでは最初から適用されません。
/* マウスなどで正確にホバーできる環境にだけ適用する */
@media (hover: hover) and (pointer: fine) {
.image-container:hover::after {
opacity: 1;
}
}
この書き方にしておくと、タップしたあとホバー状態が残り続ける現象も避けられます。装飾としての切り替えなら、これで十分です。
問題は、切り替え後の画像に情報があるときです。その場合はホバーに頼らず、タッチ端末では最初から見える形にします。
/* ホバーできない環境では、2枚目を常に少しだけ見せる or 併記する */
@media (hover: none) {
.image-container::after {
opacity: 1; /* ホバー後の画像を最初から見せる */
}
}
どちらの画像も見せたいなら、CSSの切り替えをやめて2枚並べるか、タップで開くUIにしてください。要素の表示・非表示を切り替える考え方は、CSSとjQueryでアコーディオンを実装するやjQueryでドロップダウンメニューを実装する5つの方法でも同じです。
まとめ
CSSだけで画像をホバー時に切り替える方法を紹介しました。
- 切り替えるだけなら
:hoverでbackground-imageを差し替える - Chrome と Safari は
transition: background-imageでクロスフェードするようになった(2026年10月時点) - Firefox は混ざらないので、見え方をそろえるなら2枚重ねて
opacity - スライド・拡大・回転・クリップパスも、2枚重ねの応用で作れる
- ホバーできない端末のために
@media (hover: hover)で囲む
CSSの書き方をまとめて学び直したい場合は、Webコーディングにオススメの学習本に学習順をまとめています。
