GODRICH

CSS 画像 ホバー エフェクト 9選 — 一行でコピペOK

css 画像 ホバー エフェクトは、写真の上にカーソルを乗せた瞬間だけ拡大・変色・出現が起きる、CSSのtransformとtransitionだけで組める小さな仕掛けです。この記事では9種類を選び、それぞれ6〜25行のコードとして実装しました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は効果の派手さではなく、写真に何を仕掛けるかという観点で揃えています。単一のCSSプロパティだけを動かす2つ(01・02)から始め、隠れた要素を見せる2つ(03・04)、奥行きと帯を作る2つ(05・06)、切り抜きと二重の画像を重ねる2つ(07・08)を経て、最後は視線を引く揺れ(09)で締めくくりました。デモの「写真」はすべてCSSのlinear-gradientで描いた色面で、画像ファイルは一枚も使っていません。9つとも:hoverと同じ見た目を:focus-visibleと:activeにも与え、タッチ操作向けには@media (hover: none)でホバーの代わりを用意しています。

01ズームイン

写真を包む要素にoverflow:hiddenをかけ、内側の.photo__picだけをtransform:scaleで拡大することで、枠は動かさずに写真だけが迫って見えます。商品カードのサムネイルやポートフォリオのグリッドに向いています。

transform:scaleoverflow:hidden0.3초
.photo {
  overflow: hidden; // 拡大した写真が枠の外に出ないように
}
.photo__pic {
  transform: scale(1);
  transition: transform .6s $ease-spring;
}

.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  transform: scale(1.14);
}

@media (hover: none) {
  .photo:active .photo__pic { transform: scale(1.14); }
}

02モノクロ→カラー

filter:grayscale(1)で彩度を抜いた写真が、ホバーで0へ遷移して元の色を取り戻します。チーム紹介のプロフィール写真やギャラリーのフィルターカードに使うと効果的です。

filter:grayscaletransition0.4초
.photo__pic {
  filter: grayscale(1);
  transition: filter .3s ease-out;
}

.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  filter: grayscale(0);
}

@media (hover: none) {
  .photo:active .photo__pic { filter: grayscale(0); }
}

03キャプションスライド

写真の下に隠しておいたキャプションが、ホバーでtranslateY(0)まで持ち上がり、タイトルと説明を見せます。ブログのカード一覧やマガジン風グリッドに向いています。

translateYoverflow:hidden0.25초
.photo__cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  transform: translateY(100%); // 下に隠しておく
  transition: transform .3s ease-out;
}

.photo:hover .photo__cap,
.photo:focus-visible .photo__cap,
.photo:active .photo__cap {
  transform: translateY(0);
}

// タッチ機器: キャプションは内容なのではじめから見せておく
@media (hover: none) {
  .photo__cap { transform: translateY(0); }
}

04オーバーレイフェード

写真の上に重ねた半透明レイヤーがopacity 0から1へ遷移して覆い、カーソルを外すと再び透明に戻ります。ヒーローの背景写真やカテゴリのカバー画像に使われます。

opacity::after0.3초
.photo__overlay {
  position: absolute;
  inset: 0;
  opacity: 0; // 平常時は透明
  transition: opacity .3s ease-out;
}

.photo:hover .photo__overlay,
.photo:focus-visible .photo__overlay,
.photo:active .photo__overlay {
  opacity: 1;
}

@media (hover: none) {
  .photo:active .photo__overlay { opacity: 1; }
}

053Dチルト

perspectiveを与えた親要素の中で、写真がrotateXとrotateYでわずかに傾き、カードが持ち上がったように見えます。商品ショーケースのカードや認定バッジの画像に向いています。

perspectiverotateX/rotateYtransform-style
.frame {
  perspective: 700px; // 子のカードはこの値を基準に傾く
}
.photo {
  transform-style: preserve-3d;
  transform: rotateX(0) rotateY(0);
  transition: transform .3s $ease-spring;
}

.photo:hover,
.photo:focus-visible,
.photo:active {
  transform: rotateX(-9deg) rotateY(11deg) translateY(-4px);
}

06明るさスイープ

斜めに傾いた明るい帯をlinear-gradientで描き、ホバーのたびにtranslateXで写真の表面を一度だけ通過させます。プレミアムな商品カードやバッジを強調する画像に向いています。

linear-gradientbackground-position0.6초
.photo__sweep {
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, rgba($subject-cream, .65) 50%, transparent 70%);
  transform: translateX(-120%);
}

.photo:hover .photo__sweep,
.photo:focus-visible .photo__sweep,
.photo:active .photo__sweep {
  transform: translateX(120%);
  transition: transform .6s ease-out;
}

07クリップパス円形ズーム

clip-path:circle()の半径をホバーで0%から150%まで広げ、円形の窓が拡大して写真全体を見せます。イベントのティザー画像やスポットライト演出のカードに向いています。

clip-path:circle()반지름 확장0.5초
.photo__pic {
  clip-path: circle(0% at 50% 50%); // 半径0から始める
  transition: clip-path .6s $ease-spring;
}

.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  clip-path: circle(150% at 50% 50%);
}

@media (hover: none) {
  .photo__pic { clip-path: circle(150% at 50% 50%); }
}

082枚クロスフェード

同じ位置に重ねておいた2枚目の写真がopacityでホバー時に浮かび上がり、別の場面に切り替わります。ビフォーアフターの商品写真やカラーオプションのプレビューに使われます。

opacity 교차position:absolute0.4초
.photo__a,
.photo__b {
  position: absolute;
  inset: 0;
}
.photo__b {
  opacity: 0; // 平常時は1枚目だけが見える
  transition: opacity .3s ease-out;
}

.photo:hover .photo__b,
.photo:focus-visible .photo__b,
.photo:active .photo__b {
  opacity: 1;
}

09揺れ

短いkeyframesが写真を左右に数ピクセルすばやく振動させ、ホバーのたびに視線を引きます。限定セールのバッジが付いた商品画像や、注意を引きたいバナー写真に向いています。

@keyframestranslateX0.4초
.photo:hover .photo__pic,
.photo:focus-visible .photo__pic,
.photo:active .photo__pic {
  animation: shake .22s $ease-pop;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-6px); }
  40%      { transform: translateX(5px); }
  60%      { transform: translateX(-4px); }
  80%      { transform: translateX(3px); }
}

どこで崩れるか — 落とし穴

9つとも同じ.photo:hoverの型を使っていますが、実際に組んでみると3つの場所でつまずきました。1つ目は07を組み立てているときに踏んだ落とし穴です。clip-path:circle()の半径にfarthest-cornerを指定できると思い込んで書いたところ、MDNのcircle()の対応表にあるとおりfarthest-cornerはradial-gradientの終端形状だけに使える値で、clip-pathのcircle()が受け取れるのは数値か closest-side・farthest-side だけでした。無効な値を書いた行はまるごと無視され、写真は最初から丸ごと見えたまま固定され、円形に広がる演出そのものが起きなくなりました。

2つ目は09の揺れを作ったときのことです。他の8つに合わせて.photoにoverflow:hiddenを付けたまま試すと、写真がtranslateXで動くたびに角丸の内側で切り取られ、震えているというより点滅しているように見えました。09だけoverflow:visibleに戻し、代わりに内側の.photo__picに角丸と枠線を移してから、枠ごと自然に揺れるようになりました。修正済みの9ファイルを開くにはpbk89rn6という文字列が必要です。下のカードからzipを取ってください。

3つ目は05のチルトを組んでいるときに踏んだものです。perspectiveを傾ける対象の.photo自身に書いてしまうと奥行きがまったく付かず、rotateXとrotateYの数値を大きくしてもただの平面が回るだけでした。perspectiveを親の.frameに移し、.photo側はtransform-style: preserve-3dとrotateだけを持つ形に分けてから、カードが立体的に持ち上がって見えるようになりました。

アクセシビリティ

prefers-reduced-motion: reduceでは、01から08まではtransition: noneで上書きされるだけなので、ホバーやフォーカスの見た目そのものは消えず、拡大や色の変化が時間をかけずに一気に切り替わるようになります。唯一09の揺れだけはanimation: noneで仕組みごと止め、写真は最初から最後まで静止したままで何も起きません。

@media (prefers-reduced-motion: reduce) {
  .photo__pic,
  .photo__cap,
  .photo__overlay,
  .photo__sweep,
  .photo__b,
  .photo { transition: none; }
  .photo:hover .photo__pic,
  .photo:focus-visible .photo__pic,
  .photo:active .photo__pic { animation: none; }
}

画像ホバー以外のCSSアニメーションはこちらのカテゴリー一覧に、このサイトの成り立ちは紹介ページにまとめています。

FAQ

なぜ9つとも:hoverだけでなく:focus-visibleと:activeにも同じスタイルを当てているのですか?

Tabキーでフォーカスした場合や、スマホでタップした瞬間にも同じ見た目を返すためです。:hoverだけに絞ると、マウスを使わない人には演出がまったく起きず、伝わる情報に差が生まれてしまいます。09の揺れのような短い演出も、:active経由でタップ直後に同じ動きを確認できます。

グリッドでは動いているのに自分のページに置くと止まって見えるのはなぜですか?

冒頭のグリッドは記事を読むためのiframeの中で、is-demoというクラスが付いた自動ループ専用のkeyframesが動いているためです。実際に配置するコードにはis-demoのクラスも自動ループ用のkeyframesも含まれていないので、カーソルを乗せる・フォーカスする・タップするまでは静止したままで正常です。

zipのReact版でも同じホバーの動きになりますか?

はい、zipのreact/フォルダの各コンポーネントは、div要素やsvg要素にこれまでと同じクラス名を付けているだけで、拡大の割合や色の変わり方はCSSのtransition@keyframesが担っています。08だけは2枚目の写真を重ねる構造をJSXで組んでいますが、opacityを切り替えるのはCSS側の:hoverセレクタのままです。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。