CSSだけでメイソンリーレイアウト - display: grid-lanesの使い方

Xへポスト
はてなブックマークへ投稿
共有
URLをコピー

Pinterestのフィードのように、高さの違うカードを等幅の列へ隙間なく積む「メイソンリー」レイアウト。これをウェブページ内に実現しようとすると、考慮すべき点が多く、見た目以上に難しいものです。作ったことがある方なら、その難しさに心当たりがあるでしょう。作ったことがない方はどう難しいか記事を読みながら想像してみてください。

そんな難易度の高いレイアウトが、Safari 26.4から新しいレイアウトモードであるdisplay: grid-lanesを使うことで、CSSだけで実現できるようになりました。

▼ メイソンリーレイアウトの例

画像共有サイトを模したモック。高さの異なるカードが等幅の列に隙間なく積み上がった、メイソンリーレイアウトの例

本記事は、2026年6月6日に開催されたフロントエンド・PHPカンファレンス北海道2026にて、「CSS Grid Level 3 グリッドレーンのデモと、Web標準の行方」と題して発表した内容をベースにしています。 Safari 26.4での表示。全幅のheader、2列ぶんのカード、右寄せ2列のカードが混ざったメイソンリー

なぜ「メイソンリー」はCSSで難しかったのか

本記事では、高さの違うカードを等幅の列に隙間なく積むレイアウトを「メイソンリー」と呼び、それを実現するCSSの新機能をdisplay: grid-lanesと表記します。

メイソンリーでやりたいことは、突き詰めると3つの条件に絞られます。

  • カードの高さは可変(画像やテキスト量でバラバラ)
  • 列は等幅で、画面幅に応じて列数が変わる
  • HTMLに書いた順番を保ったまま、空いている列へ隙間なく詰めていく

従来のCSSには、この「最短の列を探して積む」という処理を担うレイアウトモードがありませんでした。CSS GridもFlexboxも、行や列のグリッドにきれいに揃えることは得意ですが、列ごとに高さがズレていく非対称な詰め込みには対応していません。

下の図で言えば、①〜⑥のカードをHTMLに書いた順に取り出し、そのつど積み上がりがいちばん低い列へ置いていく、という詰め方です。

①から⑥のカードをHTMLに書いた順に取り出し、そのつど積み上がりがいちばん低い列へ詰めていく、というメイソンリーの目標を示した模式図

これまでの回避策と、その限界

これまでは大きく3つの回避策が使われてきました。

回避策 代表例 限界
JSメイソンリー masonry.jsIsotopeなど リサイズや追加のたびにJSの再計算が走りカクつく。SSRとの相性の悪さ、保守の負荷も
CSS Columns(マルチカラムレイアウト) column-*プロパティ タブ順序が列単位で縦に走り、スクリーンリーダー体験が悪い
Flexboxでの列分割 flexと列コンテナー 列間の高さバランスを自前で調整する必要がある

display: grid-lanes

display: grid-lanesを使えば、専用のレイアウトモードとしてメイソンリーを実装できます。

.lanes {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(144px, 1fr));
  gap: 10px;
}

メイソンリーに必要なのは、最初の2行です。display: grid-lanesでレイアウトモードを切り替え、grid-template-columnsで列(レーン)を定義すれば、メディアクエリー不要のレスポンシブなメイソンリーになります。3行目のgapはカード間の余白指定で、ここまでがよく使う形です。

  • grid-template-columns縦方向の定義になり、縦積み(後述のwaterfall)が選択される
  • minmax(144px, 1fr)で列幅の下限(144px)と伸縮範囲(1fr)を指定する
  • auto-fillで、入る限り列を増やす

下のデモはスライダーでコンテナーの幅を変えられます。狭めていくと、メディアクエリーなしで列数が自動で減っていきます。

本記事のデモはSafari 26.4以上でご覧ください。

充填アルゴリズム — レーンが「独立に伸びる」とは

grid-lanesの挙動は以下になります。

次のカードをいちばん背の低い列に置く。これを最後まで繰り返す。

通常のCSS Gridは、行と列のグリッド線で区切られたマス目に沿ってアイテムを配置します。これに対してgrid-lanesでは、各列がレーンとして独立し、それぞれが「いまどこまで埋まっているか」という現在の実行位置を持ちます。配置のたびに、実行位置がもっとも手前にあるレーン=いちばん背の低い列が選ばれます。

順を追うと、こうなります。

  1. 各列(レーン)のいまの高さを見る
  2. いちばん低い列に次のカードを置く
  3. 最後のカードまで繰り返す

たとえば3列に①〜⑥を流すと、次のように決まります。

  • ①②③ … まず各列の先頭に順に置かれる
  • ④ … 2列目と3列目が同じ高さでいちばん低い。高さが並んだときは先(左)のレーンが選ばれるので2列目
  • ⑤ … 今度は3列目だけがいちばん低いので3列目
  • ⑥ … 残った1列目(背は高いが他が埋まった)へ

充填アルゴリズムのステップ図。①②③を順に置き、④は2列目、⑤は3列目、⑥は1列目と、毎回いちばん低い列へ配置していく過程

見た目は通常のGridと似ていますが、行をそろえずレーンごとに伸びていく点が決定的に違います。

方向の切り替え — waterfallとbrick

grid-lanesの積み上げ方向は、grid-template-columnsgrid-template-rowsのどちらを指定したかで自動的に決まります。

/* waterfall = 縦方向のメイソンリー(列を定義) */
.lanes {
  display: grid-lanes;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

/* brick = 横方向のメイソンリー(行を定義) */
.lanes {
  display: grid-lanes;
  grid-template-rows: repeat(3, 96px);
  gap: 8px;
}
  • waterfall(縦積み)grid-template-columnsを指定。一般的な縦方向のメイソンリーレイアウト
  • brick(横積み)grid-template-rowsを指定。レンガを横に積むように、左から右へ流れる(行の高さは行トラックで決まり、各カードは自分の幅を持つ。デモでは幅をばらつかせている)

brickは、RTL(右から左に読む言語)のテキストや、横スクロールするカルーセルとの相性が良い方向です。列定義か行定義かを切り替えるだけで方向が変わるため、メディアクエリーで縦横を出し分けるといった応用もできます。

アイテムのスパンと明示的配置

grid-lanesでは、アイテムの幅をCSS Gridと同じ構文で拡張できます。特定のアイテムだけ横幅を広げる、といった使い方もできます。

.lanes {
  display: grid-lanes;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
header {
  grid-column: 1 / -1;
} /* 全幅 */
.hero {
  grid-column: span 2;
} /* 2列ぶん */
.sidebar {
  grid-column: -3 / -1;
} /* 右寄せ2列 */

headerは横幅いっぱいに表示したいので、grid-column: 1 / -1を指定しています。.herogrid-column: span 2で2列ぶんの幅に広げています。.sidebar-3 / -1のようなマイナスインデックスを使うと、右端を基準にした右寄せ配置もできます。

幅の広いアイテムを複数レーンにまたがらせると、そのアイテムはまたがったレーンすべての実行位置を同時に進めます。先ほどの例ではheaderが1〜3のすべてのレーンにまたがっているため、ほかのアイテムはどれもheaderより下に配置されます。これが次に説明するflow-toleranceの挙動に影響します。

▼ デモのスクリーンショット(Safari 26.4)

Safari 26.4での表示。全幅のheader、2列ぶんのカード、右寄せ2列のカードが混ざったメイソンリー

flow-tolerance — レーン吸着の強さ

grid-lanesにあわせて新しく登場したプロパティがflow-toleranceです。これは「同じ高さのレーン」とみなす許容幅を指定します。

.lanes {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(min(144px, 30%), 1fr));
  flow-tolerance: 1em; /* 初期値のnormalは1em相当 */
}

とれる値は3種類あります。

挙動
normal(初期値) 1emを許容幅とする標準の吸着
<length-percentage> 許容幅を明示的に指定(例: 02em10%
infinite ソース順を厳守する(最短レーン優先をやめ、DOM順のまま積む)

充填アルゴリズムは「いちばん低いレーン」へ次のカードを置きますが、flow-tolerance最短レーンをどこまで厳密に優先するかを左右します。許容幅の範囲内に複数のレーンが収まっていれば、それらを「同じ高さ」とみなし、ソース順を優先して自然な並びを保てます。

  • flow-tolerance小さくすると、わずかな高さの違いでも最短レーンを厳密に選ぶため、視覚的に詰まりますが、DOM順と視覚順がずれやすくなります
  • flow-tolerance: infiniteにすると完全にソース順になり、見た目は崩れやすくなりますが順序は厳密です

下のデモはスライダーで値を変えられます。カードの高さはわざと近づけてあるので、吸着のかかり方の違いが見えやすいはずです。0に近づけてからカードをTabキーでたどると、フォーカスが画面を飛び回るのがわかります。

この値はアクセシビリティにも影響します。タブ順序やスクリーンリーダーの読み上げ順はDOM順のままなので、flow-toleranceを小さくしすぎると視覚順とのズレが広がります。先ほどのTabキーの挙動がそれで、カードが密集する画面ほど値を大きめにしておくと安心です。

JSとの連携 — anime.js / GSAP Flip

モーションの付与について

ここまでのレイアウトはCSSだけで完結しています。ただ、メイソンリーのカードは追加や並び替えのたびに位置が変わり、そのままでは一瞬で切り替わります。JavaScriptと連携すると、この位置の変化に滑らかなモーションを付けることができます。

カードの位置はレイアウト計算の結果なので、CSSのトランジションでは補間できません。滑らかに動かすには、document.startViewTransition()でブラウザに任せるか、JSライブラリで補間するかの2択です。本記事では、手軽なアニメーションライブラリを利用した方式を紹介します。

どこにカードを置くかは引き続きgrid-lanesが決めますので、移動の前後をなめらかにつなぐ補間をライブラリに任せます。デモを、anime.js版とGSAP Flip版の2パターンで用意したので、順に紹介します。

anime.js版

たとえばカードの追加時にふわっと出す演出は、次のように書けます。

import { animate } from "animejs";

// 追加したカードをふわっと表示する
animate(newCards, {
  opacity: [0, 1],
  scale: [0.5, 1],
  duration: 600,
  ease: "outQuart",
});

デモではこれをベースに、バネのイージング(spring())や時間差(stagger())も加えて、カードが弾みながら現れる演出にしています。

GSAP Flip版

FLIP(First・Last・Invert・Play)は「位置を測定 → DOMを更新 → 差分を補間」というアニメーション手法で、GSAPのFlipプラグインならFlip.from()一発で書けます。FLIPの仕組み自体は『JavaScriptで実現するFLIPアニメーションの原理と基礎』で詳しく解説しています。

import gsap from "gsap";
import { Flip } from "gsap/Flip";

gsap.registerPlugin(Flip);

function reorder(container) {
  const state = Flip.getState(".card"); // 位置を記録
  // カード要素は作り直さず、同じ要素を新しい順序へ移動させる
  // (shuffled()は配列を混ぜて返す自前の関数とします)
  shuffled([...container.children]).forEach((el) => container.append(el));
  Flip.from(state, {
    // 記録位置 → 新位置へ補間
    duration: 0.6,
    ease: "power2.inOut",
    absolute: true,
  });
}

追加・並び替え・削除のどれも、要素の前後の位置を測って差分を補間するだけなので、grid-lanesが決めた配置をそのまま活かせます。

GSAP Flipの注意点

1つハマりやすいポイントがあります。FlipはgetState()で記録した要素そのものを追跡するため、並び替えのたびにDOM要素を作り直すと補間が行われず、カードが瞬間移動してしまいます。筆者もデモの制作中にここで少し悩みました。要素は使い回して、append()で並び順だけを入れ替えるのがコツになります。

対応ブラウザ

CSSのdisplay: grid-lanesは、Safari 26.4(2026年3月)以上で利用可能です。

参照:Can I use…

まとめ

これまでJavaScriptのライブラリに任せていたメイソンリーをCSSが引き受けてくれるようになりました。Safari 26.4が手元にあれば、まずはデモを触ってみてください。

こういう複雑なレイアウトがCSSのみで実現できるようになったことで選択肢の引き出しが増えますし、このレイアウトをベースにした表現の幅を考えることができたり、より使いやすいUIの構築に頭を回したりできるようになると思いますので素直にうれしいです。

スクロールのインタラクションやWebGPU/WebGLの表現と組み合わせてもおもしろいかもしれませんね。

Grid Layoutの基礎から知りたい方は『CSSグリッド入門 - 図解でわかりやすく解説』をご覧ください。要素の幅を基準にしたレスポンシブ対応は『要素の幅でレスポンシブ対応を行える! コンテナークエリーの使い方』で紹介しています。

SNSでシェアしよう
シェアいただくと、サイト運営の励みになります!
Xへポスト
はてなブックマークへ投稿
共有
URLをコピー
楢山 哲弘

フロントエンドエンジニア。Flashからソーシャルゲーム開発、GIS開発を経て、残りの人生をフロントエンド開発に注力しようとICSに入社しました。ジェネレーティブアートに興味があります。

この担当の記事一覧