ポートフォリオ ギャラリー UI 9選 — ドラッグからモーフ切替まで
ポートフォリオ ギャラリーとは、写真や作品を格子状に並べ、ホバーやクリックで拡大・絞り込み・並び替え・切り替えが起こる画面のことです。Pinterestで保存したあのポートフォリオギャラリーを、9通りの方式に分けてそのままコードでお届けします。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 無限スクロール写真ウォール
- 02 ポラロイドスタックギャラリー
- 03 商品ギャラリー ホバーズーム
- 04 動画サムネイルグリッド 再生オーバーレイ
- 05 タグ絞り込みポートフォリオグリッド
- 06 ドラッグで並び替える写真グリッド
- 07 全幅スライドショー サムネイルストリップ
- 08 スクリムキャプションリビールギャラリー
- 09 グリッド→単一ビュー モーフ切替
9つは人気順ではなく、ギャラリーに出会う人が実際にたどる順番で並べました。最初は通り過ぎながら眺め(01)、手でめくって見て(02)、気に入った1枚を選んで拡大し(03)、動画なら再生ボタンを探し(04)、枚数が多ければタグで絞り込み(05)、持ち主なら自分で順番を整理し(06)、代表作を数枚さっと見渡し(07)、気になってキャプションを読み(08)、最後は1枚に没入して大きく見ます(09)。
| 番号 | 名前 | 何が動くか | JS |
|---|---|---|---|
| 01 | 無限スクロール写真ウォール | トラック translateX |
なし |
| 02 | ポラロイドスタックギャラリー | カード rotate・translate |
なし |
| 03 | 商品ギャラリー ホバーズーム | 写真 scale、価格バー translateY |
なし |
| 04 | 動画サムネイルグリッド 再生オーバーレイ | ボタン scale、リング scale・opacity |
なし |
| 05 | タグ絞り込みポートフォリオグリッド | カード opacity・scale |
13行(自動巡回のみ) |
| 06 | ドラッグで並び替える写真グリッド | タイル translateX |
34行 |
| 07 | 全幅スライドショー サムネイルストリップ | トラック translateX、サムネイル枠 |
19行 |
| 08 | スクリムキャプションリビールギャラリー | スクリム opacity、キャプション translateY |
なし |
| 09 | グリッド→単一ビュー モーフ切替 | タイル translate・scale |
24行 |
01無限スクロール写真ウォール
小さな正方形の写真が横に途切れず流れ続けます。帯をまるごと複製して後ろに継ぎ足しておくと、トラックが自分の幅のちょうど半分(-50%)動いた瞬間に画面が最初とまったく同じ見た目になるので、継ぎ目がわかりません。
.wall-track {
display: flex;
width: max-content; // 写真5枚 × 2セット = 10コマ
animation: wall-loop $duration linear infinite;
}
.wall-tile {
flex: 0 0 auto;
margin-right: $sp-2; // gap ではなく margin — でないと -50% がずれる
}
@keyframes wall-loop {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
02ポラロイドスタックギャラリー
写真4枚がポラロイドのようにわずかに角度をずらして重なり、一番上の1枚が横にずれて回転しながら退くと次の写真が現れる、という流れを順番に繰り返します。1枚ずつめくって見せたい旅行アルバムに向いています。
.polaroid--top {
transform: rotate(2deg) translate(1px, -4px);
z-index: 4;
animation: shuffle $duration $easing infinite;
}
@keyframes shuffle {
0%, 10%, 55%, 100% { transform: rotate(2deg) translate(1px, -4px); opacity: 1; }
30% { transform: rotate(16deg) translate(56px, -26px) scale(.94); opacity: .25; }
}
03商品ギャラリー ホバーズーム
商品写真のマスにカーソルを乗せると、枠のサイズはそのままで中の写真だけが拡大し、同時に下から価格バーがスライドで現れます。価格をさりげなく見せたいショップの商品一覧に使えます。
.pcard { overflow: hidden; } // 枠の大きさは変わらない
.pcard__img {
transition: transform $duration $easing;
}
.pcard__price {
transform: translateY(100%); // 普段は下端に隠れている
transition: transform $duration $easing;
}
.pcard:hover .pcard__img { transform: scale(1.14); }
.pcard:hover .pcard__price { transform: translateY(0); }
04動画サムネイルグリッド 再生オーバーレイ
動画サムネイルの各マスに中央の再生ボタンがあり、カーソルを乗せるとボタンが拡大しながら波紋リングが一度広がって、再生できる動画だと知らせます。動画を数本まとめて見せるメディアページに向いています。
.vcard__ring {
border: 2px solid $subject-cream;
opacity: 0;
animation: ring-pulse $duration $easing infinite;
}
@keyframes ring-pulse {
0%, 15% { transform: scale(1); opacity: 0; }
30% { opacity: .9; }
60% { transform: scale(1.9); opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
05タグ絞り込みポートフォリオグリッド
カードごとにカテゴリータグが付いており、上部のタグチップをクリックすると該当しないカードがJavaScriptなしで:has()セレクタだけで薄く縮んで消えます。Web・アプリ・ブランディングを分けて見せるエージェンシーの紹介ページに合います。
.pf-frame:has(.pf-check:checked) .pf-card {
opacity: .18;
transform: scale(.92);
}
.pf-frame:has(input[data-cat="web"]:checked) .pf-card[data-cat="web"] {
opacity: 1;
transform: scale(1);
}
06ドラッグで並び替える写真グリッド
写真のマスを指やマウスで押して隣のマスへ引っ張ると2枚の位置が入れ替わり、離すと残りのマスも自然に整列し直します。代表写真を自分で選ぶ商品画像管理画面に使います。
handle.addEventListener('pointerdown', e => {
dragging = true; startX = e.clientX;
handle.setPointerCapture(e.pointerId);
});
handle.addEventListener('pointermove', e => {
if (!dragging) return;
dx = e.clientX - startX;
row.style.transform = `translateX(${dx}px)`;
});
handle.addEventListener('pointerup', () => {
dragging = false;
const target = dx > 30 ? row.nextElementSibling
: dx < -30 ? row.previousElementSibling : null;
row.style.transform = '';
if (target) list.insertBefore(dx > 0 ? target : row, dx > 0 ? row : target);
dx = 0;
});
07全幅スライドショー サムネイルストリップ
大きな写真1枚が画面幅いっぱいに表示されて自動で切り替わり、下のサムネイル列では今表示中の写真の枠だけが光ります。代表作数点を順番にめくるポートフォリオのヒーローに向いています。
.ss-track {
width: 500%; // 実写真4枚 + 1枚目のクローン1枚 = 5コマ
animation: slide-loop $duration linear infinite;
}
@keyframes slide-loop {
0%, 18% { transform: translateX(0%); }
20%, 38% { transform: translateX(-20%); }
40%, 58% { transform: translateX(-40%); }
60%, 78% { transform: translateX(-60%); }
80%, 100% { transform: translateX(-80%); } // クローンの位置は1枚目と同じ絵
}
08スクリムキャプションリビールギャラリー
普段は写真だけが見え、カーソルを乗せると画像全体に暗いスクリムが広がるように現れ、中央にタイトルが浮かび上がります。端から帯が上がる方式と違って写真全体が暗くなるので、写真そのものが読み取りやすいままです。
.cr-card__scrim {
background: radial-gradient(120% 120% at 50% 65%, transparent 0%, rgba($stage-ink, .86) 100%);
opacity: 0;
transition: opacity $duration $easing;
}
.cr-card:hover .cr-card__scrim { opacity: 1; }
.cr-card:hover .cr-card__caption { opacity: 1; transform: translateY(0); }
09グリッド→単一ビュー モーフ切替
マスを1つクリックしてもモーダルは開かず、グリッド自体が組み替わり、選んだマスがその場で拡大し残りのマスはその場で薄く縮みます。詳細パネルを持たない、写真そのもので見せるポートフォリオに使います。
.g-grid:has(.g-radio:checked) .g-tile:not(:has(.g-radio:checked)) {
opacity: .22;
transform: scale(.88);
}
// マスごとの格子内の位置が固定なので、拡大するときの移動量もマスごとに固定値になる
.g-grid .g-tile:nth-child(1):has(.g-radio:checked) {
transform: translate(64px, 32px) scale(3.29, 2.14);
}
どこで壊れるか — 落とし穴1つ
05(タグ絞り込み)と07(サムネイルストリップ)を初めてスマホ幅(320px)でレンダリングしたとき、縦方向に内容がはみ出してマスが切れました。どちらも min(240px, 82vw) のように横幅だけをビューポート基準で縮めていたため、横幅は縮んでも行数(2行)とカードの比率(4:3)はそのままで、実際の縦の高さがスマホ画面の200px予算を大きく超えていたのです。zipのパスワード bhpgj3rm で開く vanilla フォルダの中で、05はカードを2行から1行4マスに広げ縦長の3:4比率に変え、07は全体の幅をさらに200pxまで縮めてようやく収まりました。横幅しか見ない min()・vw のレスポンシブは、縦に長いレイアウトでは別途高さを確認する必要があるということです。
アクセシビリティ
モーション低減設定(prefers-reduced-motion: reduce)をオンにした画面では、9項目すべてトランジションとループだけが止まり、最終状態はそのまま残るようにしています。01はトラックが止まって最初のコマのまま表示され、02・04・08は巡回アニメーションなしで既定の位置に固定され、03・06・09は拡大・移動が一瞬で適用され、07はスライドがトランジションなしでその場で切り替わります。06の写真タイルはポインターでのドラッグだけでなく左右矢印キーでも順番を変えられ、09のマスもネイティブのラジオボタンなので、Tabキーと矢印キーだけで写真を選んで大きく見られます。ポインターイベントの仕様はMDN Pointer Eventsで、:has() セレクタの対応状況はMDN :has()で確認できます。同じgallery系統の他の2編、CSS フォト ギャラリー 9選とビフォーアフター スライダー UI 9選もあわせてご覧ください。
FAQ
05の:has()フィルターはすべてのブラウザで動きますか。
Chrome・Edge・Safariは以前から対応しており、Firefoxもバージョン121から対応しています。セレクタに対応していない古いブラウザでは単に無視されるだけで、フィルターだけが効かなくなりカードは全部表示されたままなので、レイアウトが崩れることはありません。
09で写真が元の比率と違って伸びて見えるのはなぜですか。
マスが拡大するとき、縦横で違う倍率のscale(3.29, 2.14)をかけているためです。正方形のマスを格子全体の横に長い領域まで広げるにはこの比率差が必要で、実際の写真を使うときは.g-tile__imgにobject-fit: coverを付ければ、マスの伸びに関係なく写真自体は元の比率を保てます。
06のドラッグはタッチスクリーンでも使えますか。
使えます。pointerdown・pointermove・pointerupはマウス・タッチ・ペンを1つのAPIでまとめて扱うので指でのドラッグも同じように動作し、各タイルにtouch-action: noneを指定してドラッグ中に画面ごとスクロールしてしまうのも防いでいます。