CSS フォト ギャラリー 9選 — メイソンリーからライトボックスまで
CSS フォト ギャラリーは、複数の写真をグリッドやウォーターフォール状に並べ、ホバーやクリックに反応させるレイアウト技法です。Pinterestで保存したあのギャラリーのように見えても、実際にコードに落とすとグリッドの数式や画像のクロップでつまずきがちです。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 均等2カラムグリッド
- 02 3カラムグリッド・比率統一
- 03 自動レスポンシブグリッド
- 04 マルチカラム・ウォーターフォール
- 05 グリッド行スパン・ウォーターフォール
- 06 正方形グリッド固定
- 07 先頭強調レイアウト
- 08 ホバーキャプションオーバーレイ
- 09 クリックで開くライトボックス(JSなし)
9つは人気順ではなく、実際にギャラリーを作るときにぶつかる順番に並べました。まず一番シンプルな均等グリッドから始め(01)、写真の比率がバラバラという現実的な問題を解決し(02)、画面幅に合わせて列数が自動で変わるレスポンシブを加え(03)、高さの違う写真を隙間なく積むウォーターフォールを2通りで比べ(04〜05)、Instagramのように正方形だけに揃え(06)、代表写真1枚を大きく見せる編集レイアウトを乗せ(07)、最後にキャプションとクリック拡大というインタラクションを付け加えます(08〜09)。
| # | 名前 | 何が動くか | JS |
|---|---|---|---|
| 01 | 均等2カラムグリッド | タイル translateY・scale |
なし |
| 02 | 3カラムグリッド・比率統一 | 写真 scale、フレーム translateY |
なし |
| 03 | 自動レスポンシブグリッド | タイル opacity・scale の登場 |
なし |
| 04 | マルチカラム・ウォーターフォール | タイル translateY |
なし |
| 05 | グリッド行スパン・ウォーターフォール | タイル scale |
なし |
| 06 | 正方形グリッド固定 | 写真 scale、フレーム translateY |
なし |
| 07 | 先頭強調レイアウト | タイル scale |
なし |
| 08 | ホバーキャプションオーバーレイ | キャプション translateY・opacity |
なし |
| 09 | クリックで開くライトボックス | パネル scale・opacity |
なし |
01均等2カラムグリッド
写真4枚を2×2の均等なマスに並べ、カーソルが乗ったタイルだけがわずかに浮き上がり、影が濃くなって、今見ている写真がどれかを示します。ポートフォリオのサムネイル4〜6枚や、ブログ記事内の写真の並びのような、一番シンプルなギャラリーに使います。
.gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: $sp-2;
}
.tile {
transition: transform $duration $easing, box-shadow $duration $easing;
}
.tile:hover, .tile:focus-visible {
transform: translateY(-4px) scale(1.03);
box-shadow: $shadow-raised;
}
023カラムグリッド・比率統一
縦長・横長・正方形の写真が混ざっていても、各画像に aspect-ratio: 4/3 と object-fit: cover を指定すればすべてのマスが同じサイズに揃い、ホバーするとそのフレームの中だけ少し拡大します。スマホで撮って比率がバラバラな写真を並べるギャラリーに向いています。
.cell {
aspect-ratio: 4 / 3;
overflow: hidden;
}
.photo {
width: 100%; height: 100%;
object-fit: cover;
transform: scale(1);
transition: transform $duration $easing;
}
.cell:hover .photo, .cell:focus-within .photo {
transform: scale(1.12);
}
03自動レスポンシブグリッド
grid-template-columns: repeat(auto-fit, minmax(56px, 1fr)) の一行だけで、メディアクエリなしで画面幅に応じて1行に入るマス数が自動で変わり、新しいタイルは薄く現れて定位置に収まります。カテゴリごとに写真枚数が違う商品ギャラリーに使います。
.gallery {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(56px, 1fr));
gap: $sp-2;
}
.tile {
opacity: .35;
transform: scale(.88);
transition: transform $duration $easing, opacity $duration $easing;
}
.tile.is-visible {
opacity: 1;
transform: scale(1);
}
04マルチカラム・ウォーターフォール
写真5枚に columns: 2 を指定するだけで、JavaScriptなしでも高さがバラバラな写真がカラムに沿って隙間なく積み重なるウォーターフォールになり、break-inside: avoid で写真がカラムの境目で真っ二つに切れるのを防ぎます。Pinterest風のムードボードの定番レイアウトです。
.gallery {
columns: 2;
column-gap: $sp-2;
}
.tile {
break-inside: avoid;
margin: 0 0 $sp-2;
transition: transform $duration $easing;
}
.tile:hover, .tile:focus-visible {
transform: translateY(-3px);
}
05グリッド行スパン・ウォーターフォール(読む順番を維持)
grid-auto-rows を4pxのような極小単位に刻み、写真ごとに自分の高さに合わせた grid-row-end: span N を計算しておくと、カラム方式と違って写真が上から下ではなく左から右へ読む順番どおりに並ぶウォーターフォールになります。ニュース一覧のように掲載順が意味を持つ場面に向いています。
.gallery {
display: grid;
grid-auto-flow: row dense;
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 4px;
column-gap: $sp-2;
}
.tile--1 { grid-row-end: span 10; }
.tile--2 { grid-row-end: span 13; }
.tile--3 { grid-row-end: span 9; }
06正方形グリッド固定
すべてのタイルに aspect-ratio: 1 / 1 と object-fit: cover を指定し、元の写真の比率に関係なくInstagramのフィードのような完全な正方形だけを並べます。ホバーするとタイルがわずかに浮き上がり、中の写真も一緒に拡大します。
.cell {
aspect-ratio: 1 / 1;
overflow: hidden;
}
.photo {
width: 100%; height: 100%;
object-fit: cover;
transform: scale(1);
transition: transform $duration $easing;
}
.cell:hover .photo {
transform: scale(1.1);
}
07先頭強調レイアウト
最初の写真だけに grid-column: span 2 と grid-row: span 2 を与えて他の4倍の大きさで目立たせ、その周りに小さな写真を並べる雑誌風レイアウトを、CSSセレクタ一つで作ります。代表写真1枚を強調するブログのヘッダーギャラリーに使います。
.gallery {
display: grid;
grid-template-columns: repeat(3, 56px);
grid-template-rows: repeat(2, 56px);
gap: $sp-2;
}
.tile:first-child {
grid-column: 1 / 3;
grid-row: 1 / 3;
}
08ホバーキャプションオーバーレイ
普段は写真の下端にわずかに隠れている半透明の帯が、ホバーすると translateY で完全に上がってきて、タイトルや場所などのキャプション文字も一緒に現れます。写真ごとに地名を添える旅行ギャラリー向けです。
.caption {
position: absolute; left: 0; right: 0; bottom: 0;
transform: translateY(64%);
opacity: 0;
transition: transform $duration $easing, opacity $duration $easing;
}
.tile:hover .caption, .tile:focus-within .caption {
transform: translateY(0);
opacity: 1;
}
09クリックで開くライトボックス(JSなし)
タイルをクリックすると popovertarget 属性ひとつで原寸大の写真を収めたポップオーバーが開き、::backdrop が背景を暗く覆う間に @starting-style が写真を小さく透明な状態から始めて大きく表示します。<button popovertarget="lb-01"> と <div id="lb-01" popover> を対にするだけで、開閉のロジックはすべてブラウザが引き受けます。
.lightbox {
opacity: 0;
transform: scale(.8);
transition: opacity $duration $easing, transform $duration $easing,
overlay $duration $easing allow-discrete, display $duration $easing allow-discrete;
@starting-style { opacity: 0; transform: scale(.8); }
}
.lightbox:popover-open { opacity: 1; transform: scale(1); }
.lightbox::backdrop { background: rgba($stage-ink, .7); opacity: 0; }
.lightbox:popover-open::backdrop { opacity: 1; }
どこで壊れるか — 落とし穴ひとつ
05を最初にレンダリングしたとき、カラムの並び順が想定と違っていました。dense を付けない普通の grid-auto-flow: row では、大きくスパンするタイルの後ろにできた隙間をブラウザが埋めずに次の行へ進んでしまうことがあり、dense を付けるとその隙間を後ろのタイルが埋めてくれます。ただし dense はDOMの順番より隙間を埋めることを優先するため、スパンの組み合わせによってはタイルが実際のDOM順序と異なる位置に描画されることがあります。04(マルチカラム)は定義からして上から下・左カラムからの順なのでこの問題は起きませんが、05(グリッド行スパン)は dense を有効にした時点でその可能性を抱えることになります。掲載日のように順番そのものに意味があるギャラリーなら、dense を外して隙間を許容するか、圧縮ファイルのパスワード24p2nyh2で開けるvanillaフォルダのspan値を自分で調整し、あらかじめ隙間が出ないようにしておく必要があります。
アクセシビリティ
prefers-reduced-motion: reduce が有効な画面では、9つとも遷移とループだけを止め、途中状態ではなく完成した状態がそのまま残るようにしています。01・02・06は拡大や移動が瞬時に適用され、03のタイルはフェードインなしで最初から定位置(不透明・原寸)に現れ、04・05は浮き上がらずに静止したままで、07の強調タイルは弾む演出なしでそのままの大きさを保ち、08はキャプションが常に開いた状態で固定され、09のライトボックスは拡大の遷移なしで開閉します。09のトリガーはネイティブの <button> 要素なので、クリックと同じようにキーボード(Tab、続けてEnter)でも開き、popovertargetaction="hide" の閉じるボタンも同じ方法で操作できます。残り8項目もホバー専用の動きを :focus-visible や :focus-within にも適用しているため、キーボード操作でもマウス操作と同じ結果が得られます。CSS Grid メイソンリー提案のブラウザ対応状況はMDNで、Popover APIの対応表はMDNのPopover APIページで確認できます。カード型のホバー効果に興味があれば、カードホバー集やグラスモーフィズム集も参考になります。
FAQ
09のライトボックスはPopover APIに対応していないブラウザではどうなりますか?
対応していないブラウザは popovertarget 属性を単に無視するため、ボタンはただのボタンのままでクリックしても何も起きません。レイアウトが崩れたりエラーが出たりしない、安全な性能劣化です。最新のChrome・Edge・Safari・Firefoxはすでに対応しています。
04と05、どちらのウォーターフォールを使えばいいですか?
写真が上から下・次のカラムへという順で読まれても構わない場合(ムードボードや一般的なギャラリー)は、columns 方式(04)のほうがコードが短く保守しやすいです。「新着順」のように左上から順番どおりに読ませたい場合はグリッド方式(05)を使い、上の落とし穴で触れた dense による順序の入れ替わりを見込んでおいてください。
React版の03自動レスポンシブグリッドは、カラム数をどうやって把握していますか?
計算していません。AutoFitGallery はリサイズイベントも状態も持たず、grid-template-columns: repeat(auto-fit, minmax()) がブラウザのレイアウトエンジンの中でコンテナ幅だけを見て決めているため、ウィンドウサイズが変われば自動的に並びが変わります。