GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

【コピペOK】CSSベントグリッドホバーアニメーション9選

ポートフォリオサイトや製品紹介ページで、大きな四角と小さな四角が写真のコラージュのように組み合わさったレイアウトを見たことはないでしょうか。あれはベントグリッドと呼ばれるレイアウトで、この記事ではマス目にマウスを乗せたときの反応を作るCSSコードを9つ、一行でコピペOKな形で集めました。

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

9つは人気順ではなく、ユーザーがグリッドを目で追う順番に並べています。マス全体がふわっと反応するだけの軽い合図(01〜02)から、中の写真やアイコンだけが動くコンテンツ単位の反応(03〜05)へ進み、数字が変わる瞬間(06)とレイアウトそのものを操作したように見える大胆な演出(07)を経て、最後は画面に入ってきた瞬間(08)とグリッド全体を照らす光の演出(09)で締めくくります。タイルごとに光を載せて反応を強めるならCSSグロー効果がその材料を渡し、タイルの動く速さそのものを整えたいならイージングカーブとスプリングが曲線の一覧になります。

01呼吸するような拡大+影

カード ホバー アニメーション をどこで使うか

マウスを乗せるとタイル全体がわずかに拡大しながら影が深くなり、まるで息を吸うようにゆっくり反応します。

呼吸するような拡大+影
新しいタブで開く

01 · 呼吸するような拡大+影 — 計測 累積面積 2.481% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scalebox-shadow300ms

なぜこう書くか

動いているのはtransform: scale(1.04)とbox-shadowだけで、レイアウトの位置やサイズは一切崩れません。

.bs-focal {
  transition: transform 300ms $ease-spring, box-shadow 300ms $ease-spring;
  box-shadow: $shadow-press;
}
.bs-focal:hover {
  transform: scale(1.04);
  box-shadow: $shadow-raised;
}

02ボーダービームタイル

どの画面か

強調したいタイルにマウスを乗せると、枠線に沿って細い光が一周します。

ボーダービームタイル
新しいタブで開く

02 · ボーダービームタイル — 計測 累積面積 5.527% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

conic-gradientmask1.6s

実装の核

conic-gradientをmask-composite: excludeでくり抜いてリング状にし、光っているように見えるのはこのレイヤーのtransform回転だけです。

.bb-focal::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: $sp-1;
  background: conic-gradient(from 0deg, transparent 0 70%, $color 85%, transparent 100%);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: 0;
}
.bb-focal:hover::before {
  opacity: 1;
  animation: bb-spin 1.6s linear infinite;
}

03画像タイルズーム

何が動くか

サムネイルの入ったタイルにマウスを乗せると、外枠は動かないまま中の写真だけがゆっくり拡大します。

画像タイルズーム
新しいタブで開く

03 · 画像タイルズーム — 計測 累積面積 2.692% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleoverflow: hidden400ms

何が肝心か

overflow: hiddenで額縁を固定し、その内側の画像レイヤーにだけtransform: scaleをかけている仕組みです。

.iz-tile--a { overflow: hidden; }
.iz-tile__media {
  position: absolute;
  inset: 0;
  transition: transform 600ms linear;
}
.iz-focal:hover .iz-tile__media {
  transform: scale(1.15);
}

04アイコンフロートタイル

どこで使うか

機能紹介タイルの中の小さなアイコンは普段は静止していて、マウスを乗せて初めて上下にふわふわ浮き始めます。

アイコンフロートタイル
新しいタブで開く

04 · アイコンフロートタイル — 計測 累積面積 0.461% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateY@keyframes1.4s

押さえておく点

ホバーしていない間は完全に止まっているので、グリッド全体を見たときに視線が散りません。

.if-focal:hover .if-icon {
  animation: if-bob 1.4s $ease-out infinite;
}
@keyframes if-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

05グラデーションリビールタイル

見えるもの

普段は写真だけが見えているタイルに、マウスを乗せると下から暗いグラデーションの幕がじわりと現れ、タイトルの文字が少しだけ上に動きます。

グラデーションリビールタイル
新しいタブで開く

05 · グラデーションリビールタイル — 計測 累積面積 13.982% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

opacitytranslateY350ms

どう作るか

動いているのは幕のopacityとタイトルのtransformだけで、写真そのものには一切手を加えていません。

.gr-tile__scrim {
  opacity: 0;
  transition: opacity 350ms $ease-out;
}
.gr-tile__label {
  transform: translateY(8px);
  transition: transform 350ms $ease-out;
}
.gr-focal:hover .gr-tile__scrim { opacity: 1; }
.gr-focal:hover .gr-tile__label { transform: translateY(0); }

06統計カウントタイル

どんな場面か

数字で実績を見せるタイルにマウスを乗せると、0から目標の数字までカウントアップします。@propertyで整数型として登録したカスタムプロパティ--numをキーフレームで動かし、実際に画面に出ている数字は::afterのcounter()が描いています。

統計カウントタイル
新しいタブで開く

06 · 統計カウントタイル — 計測 累積面積 0.54% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@propertycounter()1.2s

コードの読み方

このプロパティをinherits: trueで登録しないと::afterのcounter()が更新されず数字がまったく動かなくなるので、登録の仕方には注意が必要です。

@property --num {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0;
}
.sc-stat__value::after {
  counter-reset: num var(--num);
  content: counter(num);
}
@keyframes sc-countup { to { --num: 87; } }
.sc-focal:hover .sc-stat__value {
  animation: sc-countup 1.2s $ease-out forwards;
}

07ホバーでgrid span拡張

何が起きるか

マウスを乗せたタイルが隣のマスまで広がって大きくなったように見えますが、実際にgrid-columnやgrid-rowのspan値が変わっているわけではありません。

ホバーでgrid span拡張
新しいタブで開く

07 · ホバーでgrid span拡張 — 計測 累積面積 13.703% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scalez-indextransform-origin: top left

コードの要点

span値はとびとびの離散値でトランジションが効かないため、代わりにtransform: scaleとz-indexで隣のマスの上に重ねることで拡大を再現しています。

.he-focal {
  transition: transform 600ms $ease-spring, box-shadow 600ms $ease-spring;
  box-shadow: $shadow-press;
}
.he-focal:hover {
  transform: scale(1.35);
  box-shadow: $shadow-raised;
  z-index: 5;
}

08スクロールで順番に表示

画面で起きること

グリッドが画面に入ってくると、タイルが一つずつ時間差で浮かび上がって現れます。

スクロールで順番に表示
新しいタブで開く

08 · スクロールで順番に表示 — 計測 累積面積 36.644% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

IntersectionObservertransition-delayJS 8줄

動く仕組み

実際のコードはIntersectionObserverがタイルの交差を検知して.inクラスを付け、タイルごとにtransition-delayの値だけを変えることで順番に見せています。

var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    if (e.isIntersecting) {
      e.target.classList.add('in');
      io.unobserve(e.target);
    }
  });
}, { threshold: 0.3 });
tiles.forEach(function (t) { io.observe(t); });
.ss-tile {
  transition: opacity 400ms $ease-out, transform 400ms $ease-out;
}
.ss-tile.in { opacity: 1; transform: translateY(0); }
.ss-tile:nth-child(2).in { transition-delay: 160ms; }
.ss-tile:nth-child(3).in { transition-delay: 320ms; }
.ss-tile:nth-child(4).in { transition-delay: 480ms; }

09隣接タイルのグロー

見え方

グリッドの上でマウスを動かすと、今ポイントしているマスだけでなく隣のマスまでやわらかい光が一緒ににじみます。

隣接タイルのグロー
新しいタブで開く

09 · 隣接タイルのグロー — 計測 累積面積 36.649% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

:has()radial-gradientJS 8줄

組み立て方

カーソル座標を--mx・--myというカスタムプロパティに保存して全タイルで共有し、:has()で「グリッドのどこかがホバー中か」だけを判定して光のopacityを切り替えています。

.sl-tile::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(140px circle at var(--mx) var(--my), rgba($color, .6), transparent 70%);
  opacity: 0;
  transition: opacity 200ms linear;
}
.sl-grid:has(.sl-tile:hover) .sl-tile::before { opacity: 1; }
var grid = document.querySelector('.sl-grid');
grid.addEventListener('pointermove', function (e) {
  var r = grid.getBoundingClientRect();
  var x = ((e.clientX - r.left) / r.width * 100).toFixed(1) + '%';
  var y = ((e.clientY - r.top) / r.height * 100).toFixed(1) + '%';
  grid.style.setProperty('--mx', x);
  grid.style.setProperty('--my', y);
});

ベントグリッドレイアウトはどこで壊れるか

何が問題か

カード レイアウト デザイン を作るうえで9つの中でいちばんつまずきやすいのは07番です。マウスを乗せたマスをgrid-column: span 2のように広げたいと考える人は多いのですが、gridのspan値はとびとびの離散値なのでブラウザが中間のコマを作れず、トランジション自体が発生しません。切り替わった瞬間にワープするだけです。そのため実際のマス割りはそのままにして、transform: scaleとz-indexで隣のマスの上に重ねる方法を使います。

どう避けるか

1マスだけを大きく見せたいレイアウトを探しているならベントグリッドレイアウトの作り方もあわせて読めますし、カードそのものが反応するパターンはCSSカードホバーアニメーション9選にまとめています。

09番の:has()セレクターと06番の@propertyによるカウンターも比較的新しい機能で、Chrome・Edge・最新のSafari・Firefoxでは問題なく動きますが、古いブラウザでは動きのない最終状態のまま表示されます。08番には、スクロール位置だけでアニメーションを駆動するanimation-timeline: view()というネイティブCSS機能もありますが、ブラウザごとにレンダリング結果が割れるためこのデモでは採用せず、どこでも同じように動くIntersectionObserverを実コードとして使っています。zipを解凍するときに求められるパスワードは6s9wc2fbで、そのまま入力すれば開きます。

アクセシビリティ — 9つがreduced-motionで残すもの

reduced-motion で残るもの

システムで動きを減らす設定をオンにすると、9つ全部がprefers-reduced-motion: reduceでanimation: none !importantになり動きが止まります。01・02・03・05・07・09のような装飾的な反応はそのまま静止していれば十分ですが、06のカウントアップと08の順番に現れる動きは情報そのものが動きで伝わるため、アニメーションを止める代わりに--numやopacity・transformを最初から最終値に固定し、動かなくても数字とコンテンツが最初から全部見えるようにしてあります。

あわせて読みたい

ひとつのタイルが大きくなると隣が押し出される問題は、配置そのものを設計するベントグリッドのLPパーツでグリッド規則として先に解いてあります。

FAQ — よくある質問

ベントグリッドを作るのにgridは必須ですか?

はい、display: gridとgrid-template-columnsでマスの大きさを変えて配置するのがベントグリッドの土台です。この記事の9つのデモはホバーの反応だけを見せるために3〜4マスの小さなグリッドを使っていますが、実際のページではマスの数とspan値を増やすだけで応用できます。

css カード ホバー はマウスのないタッチ端末では効かないのでは?

そのとおりです。タッチ端末では:hoverがまったく反応しないか、一度タップしたまま外れない場合が多いため、01・03・05のような見た目だけの演出はタッチで省略されても情報が欠けないように設計してあります。06のように数字が重要な項目は、画面に入ってきたタイミング(ビューポート進入)をきっかけに切り替える方法をおすすめします。

zipの中のvanillaとreactでコードは違いますか?

動きは同じです。vanilla版はSCSS変数をファイルの先頭にまとめていて、React版は同じ値をコンポーネントのpropsとして受け取り、CSSのカスタムプロパティに渡す構造になっています。見た目の結果はどちらも変わりません。タイルを作品一覧で埋めて並べ替えや拡大まで付ける予定なら、ポートフォリオギャラリーのUIが次の段になります。

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

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