CSSで画像に鏡面反射をつける方法(ホバーアニメーション付き)

はじめに

以前、このサイトに『今月の名盤』というアルバムジャケットを飾れるスペースを追加したのですが、その際、cssによって鏡面反射やマウスホバー時のアニメーションを実装しました。

(※2024/11/01追記:現在は少し修正して、鏡面反射効果はなくしています。)

今回はその実装方法について解説(あるいは、自分用メモ)していきたいと思います。

完成形はこんな感じです。水面に反射したような効果が画像の下側に付いています。(画像が灰色で分かりづらいですが。。。)

鏡面反射画像の完成図

概要

解説の前にコードの全体像を示しておきます。

(※2024/11/01追記:画像の角丸効果のコードを修正しました。)

HTML

<div class="month_album_cover">
    <div class="month_album_cover_original">
        <a href="リンク先のURL">
            <img src="画像のURL" alt="ジャケット画像">
        </a>
    </div>
    <div class="month_album_cover_reflect">
        <img src="画像のURL" alt="ジャケット鏡面反射">
    </div>
</div>

CSS

.month_album_cover {
    margin: 0 auto;
    width: 70%;
}
.month_album_cover_original {
    margin-bottom: 5px;
    overflow: hidden;
    border-radius: 3px;  /* 修正 */
}
.month_album_cover_reflect {
    position: relative;
    overflow: hidden;
    border-radius: 3px;  /* 修正 */
}

.month_album_cover img{
    width: 100%;
    display: block;
    /* border-radius: 3px; */
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
}
.month_album_cover_reflect img {
    transform: scaleY(-1);
    object-fit: cover;
    object-position: bottom;
    aspect-ratio: 4 / 1;
}
.month_album_cover_reflect::after {
    content: "";
    width: 100%;
    height: 100%;
    display: block;
    position: absolute;
    top: 0;
    background: linear-gradient(rgba(255, 255, 255, 0.5), #fff);
}

.month_album_cover_original:hover img {
    transform: scale(1.1);
    opacity: 0.8;
}
.month_album_cover_original:hover ~ .month_album_cover_reflect img {
    transform: scale(1.1, -1.1);
    opacity: 0.8;
}

鏡面反射

画像に鏡面反射の効果を手っ取り早く付けたい場合、反射させたい要素に-webkit-box-reflectを使うというやり方があります。ただし、この方法はWebkitに対応しているブラウザでないと表示されず、MDNのドキュメントでも「非標準」となっており、個人的にはあまり望ましくない印象です。

そこで、今回はcssで一から鏡面反射効果を実装することにしました。(一からとはいっても、既に同じことをやっているサイトが複数あったのでその辺りを参考にさせていただきました)

準備

作り方は単純で、元の画像の下に上下反転した画像を置き、その上からグラデーション(透明~白)のマスクをかぶせることで、水面に映る反射のように見せるという方法です。実際、前述したHTMLファイルの要素を図で表すと、以下のように2枚の画像が上下に並んだ構造になっています。

HTML要素の構造図

今回は拡大アニメーションをつけるため、div要素で2つの画像領域を作り、その中にimg要素を収めるという形を取っています。(それぞれのdiv要素にoverflow: hiddenを適用しているのはそのためです。また、position: relativeについては後述します。)

.month_album_cover_original {
    margin-bottom: 5px;
    overflow: hidden;
}
.month_album_cover_reflect {
    position: relative;
    overflow: hidden;
}

画像の反転

反射画像はtransform: scaleY(-1)によって下の画像を上下反転させることで作ります。

transform: scaleY(-1)以下の3つのプロパティは、反射画像を縦横比1:4にトリミングするためのものです。画像全体を反射させたい場合はなくても構いません。

.month_album_cover_reflect img {
    transform:aleY(-1);
    object-fit: cover;
    object-position: bottom;
    aspect-ratio: 4 / 1;
}

画像のフェードアウト

反射画像が下に行くほどフェードアウトしていく効果は、疑似要素である::afterを利用して実現します。

::afterは選択した要素の最後の子要素として、新しい疑似要素(htmlファイル上にない要素)を生成します。(最初の子要素として生成する::beforeもあります)

公式ドキュメント (MDN)

疑似要素のよくある使い方としては、文章の最後(最初)に文字や記号を追加する使い方が挙げられます。

HTML

<p class="important_text">こんにちは</p>

CSS

.important_text::after {
    content: "👈";
}

上のようなコードの場合、表示は次のようになります。

こんにちは

このようにして、htmlファイルに手を加えずに装飾的な要素を追加することができます。(html上に直接要素を加えることもできますが、デザインに関するものはできるだけhtmlからcssに分離したほうがいいっぽいです)

この疑似要素を使って、反射画像にかぶせる透明から白へのグラデーションマスクを生成したのが以下のコードになります。

.month_album_cover_reflect::after {
    content: "";
    width: 100%;
    height: 100%;
    display: block;
    position: absolute;
    top: 0;
    background: linear-gradient(rgba(255, 255, 255, 0.5), #fff);
}

div要素であるmonth_album_cover_reflectを選択して::afterを付けているため、ブラウザに表示された際の実質的なhtmlの要素の構造は以下のようになります。(chromeの「検証」などで確認できます)

<div class="month_album_cover_reflect">
    <img src="画像のURL" alt="ジャケット鏡面反射">
    ::after     <!-- 最後の子要素 -->
</div>

特に表示するものは無いので、contentは空です。(ただし、contentプロパティを消してしまうと疑似要素が生成されないので注意!!)

このままだと画像の下に疑似要素が追加されるだけなので、この要素を画像全体の上にかぶせる必要があります(下図参照)。

疑似要素の配置の変更図

これを実現するために、まずposition: absoluteを追加することで要素の位置を自由に指定できるようにします。(この時、親要素(month_album_cover_reflect)にpositionプロパティを追加することで、そこを基準に指定できるようになります。 ※参考→なぜ親要素に position: relative, 子要素に position: absolute を指定すると 子要素の位置をいい感じに指定できるのか (Qiita)

その後、top: 0で親要素の左上に要素を配置し、幅と高さを100%にして疑似要素のサイズを親要素のサイズいっぱいに伸ばします。

こうして、画像の上に疑似要素を重ねることができました。あとはbackgroundにグラデーションを指定することで、グラデーションマスクの完成です。

ホバー時のアニメーション

基本的には疑似クラスの:hoverを用いて、マウスホバー時の画像の大きさや不透明度を指定するだけです。既に両方の画像にtransitionが設定してあるため、マウスがホバーすると勝手にアニメーションしてくれます。

.month_album_cover img{
    ...
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
}

...

/* マウスホバー時 */
.month_album_cover_original:hover img {
    transform: scale(1.1);  /* 1.1倍に拡大 */
    opacity: 0.8;           /* 不透明度0.8 */
}
.month_album_cover_original:hover ~ .month_album_cover_reflect img {
    transform: scale(1.1, -1.1);
    opacity: 0.8;
}

最後のCSSセレクタが複雑な書き方になっていますが、これは元画像の領域でマウスホバーがあったときに反射画像の方も一緒にアニメーションさせるためです。

後続兄弟結合子(~)というものを用いて(初めて使った。。。)、元画像領域の兄弟要素である反射画像領域(の中のimg要素)を選択しています。

おわりに

疑似要素をこのように特殊に(?)使うのが初めてだったので、最初は中々苦戦しました。子要素として生成されるということを知ったことで、理解がしやすくなった気がします。

この疑似要素、他にも使い方次第で色んな遊びができそう。