写真ギャラリーのようなレイアウトを作るとき、CSSの columns プロパティはとても便利です。
ただ、使ってみると写真の表示順が「上から下、次の列へ」となり、直感的な「左から右、次の行へ」の順番にならなくて困った経験はないでしょうか。
今回は、行優先の表示順を維持しつつ、高さの異なる要素が隙間なく並ぶMasonryレイアウトをReactで実装する方法を紹介します。
CSS columnsの表示順の問題
写真ギャラリーを実装する際、CSSの columns はとても手軽です。
.gallery {
columns: 3;
gap: 16px;
}
.gallery-item {
break-inside: avoid;
margin-bottom: 16px;
}
これだけで、高さの異なる要素が隙間なく配置される、いわゆるMasonry風のレイアウトが完成します。
課題
しかし、columns には大きな問題があります。要素の並び順が列方向(上→下→次の列)になることです。
例えば、画像が6枚あって3列の場合:
CSS columns の表示順:
┌─────────┬─────────┬─────────┐
│ 画像1 │ 画像3 │ 画像5 │
│ 画像2 │ 画像4 │ 画像6 │
└─────────┴─────────┴─────────┘
直感的に期待する順番は、左上から右に向かって並ぶ行優先の順番ですよね。
期待する表示順:
┌─────────┬─────────┬─────────┐
│ 画像1 │ 画像2 │ 画像3 │
│ 画像4 │ 画像5 │ 画像6 │
└─────────┴─────────┴─────────┘
CSS Gridで解決できる?
「それなら CSS Grid を使えばいいのでは?」と思うかもしれません。
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
CSS Gridなら表示順は行優先になります。 ですが、各行の高さが最も高い要素に合わせられてしまうため、横長の画像と縦長の画像が混在すると大きな隙間が生まれてしまいます。
CSS Grid の問題:
┌─────────┬─────────┬─────────┐
│ │ │█████████│
│ 画像1 │ 画像2 │█画像3██│
│ │ │█████████│
│ │ │(隙間) │ ← 縦長の画像1に高さが揃えられる
└─────────┴─────────┴─────────┘
つまり、CSS columnsは隙間なしだけど順番が列優先、CSS Gridは順番が行優先だけど隙間ができるというトレードオフがあります。
解決策: JavaScriptで行優先にカラム振り分け
両方の要件を満たすには、JavaScriptで要素をラウンドロビン方式で各カラムに振り分けるのが効果的です。
考え方はシンプルで、要素をインデックス順に index % カラム数 で各列に配分します。
ラウンドロビン方式の振り分け:
画像1 → 列1 画像2 → 列2 画像3 → 列3
画像4 → 列1 画像5 → 列2 画像6 → 列3
結果:
列1: [画像1, 画像4]
列2: [画像2, 画像5]
列3: [画像3, 画像6]
各列は独立した flex-column で描画するので高さの異なる要素があっても隙間は生まれません。
しかも、左上から右に読んでいくと 1→2→3→4→5→6 の順番になります。
実装
Reactコンポーネントとして実装してみましょう。
"use client";
import { useState, useEffect, ReactNode } from "react";
// ブレークポイントに合わせたカラム数設定
const BREAKPOINTS = [
{ minWidth: 1536, columns: 4 },
{ minWidth: 1280, columns: 3 },
{ minWidth: 640, columns: 2 },
{ minWidth: 0, columns: 1 },
];
function getColumnCount(width: number): number {
for (const bp of BREAKPOINTS) {
if (width >= bp.minWidth) return bp.columns;
}
return 1;
}
export function MasonryGrid({ children }: { children: ReactNode[] }) {
const [columnCount, setColumnCount] = useState(1);
useEffect(() => {
function handleResize() {
setColumnCount(getColumnCount(window.innerWidth));
}
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// ラウンドロビンで各列に振り分け
const columns: ReactNode[][] = Array.from(
{ length: columnCount },
() => [],
);
children.forEach((child, index) => {
columns[index % columnCount].push(child);
});
return (
<div style={{ display: "flex", gap: "16px" }}>
{columns.map((columnItems, colIndex) => (
<div
key={colIndex}
style={{
flex: 1,
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
{columnItems}
</div>
))}
</div>
);
}
使い方
使い方もシンプルです。画像カードを MasonryGrid で囲むだけで完了します。
import { MasonryGrid } from "@/components/MasonryGrid";
export default function Gallery() {
return (
<MasonryGrid>
{images.map((image) => (
<ImageCard key={image.id} image={image} />
))}
</MasonryGrid>
);
}
実際の例
写真ポートフォリオサイトでMasonryLayoutを使ってみました。
beforeは縦に並んでいますが、afterは横に並んでいます。
MasonryLayoutを適用してもスペースが生まれずにうまく配置できているのがわかります。
- before

- after

各アプローチの比較
| 方式 | 表示順 | 隙間 | JS不要 |
|---|---|---|---|
CSS columns | 列優先(上→下) | なし | ✅ |
| CSS Grid | 行優先(左→右) | あり | ✅ |
| ラウンドロビン振り分け | 行優先(左→右) | なし | ❌ |
CSS だけで完結しないのは少し残念ですが、コード量も少なく、仕組みもシンプルなので十分実用的だと思います。
補足: CSS Grid の masonry 値について
実はCSSの仕様として grid-template-rows: masonry という値が提案されています
。
これが正式に採用されれば、CSS Gridだけで行優先かつ隙間のないMasonryレイアウトが実現できるようになります。
/* 将来的にはこれだけでOKになるかもしれない */
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: masonry;
gap: 16px;
}
2026年4月時点ではFirefoxでフラグ付きで試せる段階で、まだ安定して使える状況ではありません。 ブラウザの対応が進むまでは、今回紹介したJavaScriptベースの方法が現実的な解決策になるでしょう。
まとめ
CSS columns は手軽にMasonryレイアウトを実現できますが、表示順が列優先になるという制約があります。
行優先の順番にしたい場合は、JavaScriptでラウンドロビン方式に要素を各カラムに振り分けるのがシンプルで効果的です。
将来的にはCSS Grid の masonry 値で純CSSの解決策が使えるようになりそうですが、それまではこの方法がおすすめです。
同じような課題を抱えている方の参考になれば幸いです!