CSS Grid の
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));を眺めて「動く」と分かるが、なぜ動くか言えない人は多い。Holy grail レイアウト、サイドバー、レスポンシブギャラリーをドラッグなしでクリックだけで組める ビジュアル Grid エディタ を 500 行の vanilla JS で書いた。プレビューは本物の CSS Grid (ライブラリ不使用)、生成 CSS と表示が完全に一致する。
🌐 Demo: https://sen.ltd/portfolio/css-grid-builder/
📦 GitHub: https://github.com/sen-ltd/css-grid-builder
ジェネレータと UI を分離
grid.js ← normalizeTrack / renderTracks / buildGridCSS / validateTrack (DOM 非依存)
presets.js ← 7 種のレイアウト config
app.js ← UI グルー(トラック行、プリセット切替、<style> 注入)
grid.js には document も <style> も出てこない。{ columns, rows, gap, justifyItems, alignItems } を受け取って CSS 文字列を返すだけ。Node テスト 22 件で全分岐を保証する。
等価な row-gap/column-gap は gap: に畳む
ナイーブに row-gap: 12px; column-gap: 12px; と並べてもブラウザは動くが、コピーする側は手で gap: 12px; に書き直す。なら最初から畳んで出す:
const gap = config.gap || {};
if (gap.row && gap.column && gap.row === gap.column) {
lines.push(` gap: ${normalizeTrack(gap.row)};`);
} else {
if (gap.row) lines.push(` row-gap: ${normalizeTrack(gap.row)};`);
if (gap.column) lines.push(` column-gap: ${normalizeTrack(gap.column)};`);
}
「手で書いたら自然に書く形」と一致させるのがコピペツールの品質基準。
同じ理屈で justify-items: stretch; / align-items: stretch; (デフォルト値) は出力に含めない:
if (config.justifyItems && config.justifyItems !== "stretch") {
lines.push(` justify-items: ${config.justifyItems};`);
}
ユーザが UI で stretch を選んでも、コピー結果に justify-items: stretch; が混入しない。デフォルトと等価な宣言は省く ことで生成 CSS が "minimal and idiomatic" になる。
トラック式のバリデーション
CSS Grid のトラックは構文ジャングル: 1fr, 200px, 50%, auto, min-content, max-content, minmax(100px, 1fr), repeat(3, 1fr), repeat(auto-fit, minmax(140px, 1fr)), fit-content(200px)。
これを純粋関数でバリデート:
export function validateTrack(value) {
const t = normalizeTrack(value);
if (t === "") return null;
if (/^(auto|min-content|max-content)$/.test(t)) return null;
if (/^\d+(\.\d+)?(fr|px|%|em|rem|vh|vw|ch)$/.test(t)) return null;
if (/^minmax\([^)]+\)$/i.test(t)) return null;
if (/^repeat\([^)]+\)$/i.test(t)) return null;
if (/^fit-content\([^)]+\)$/i.test(t)) return null;
return `unrecognized track: ${t}`;
}
minmax(...) と repeat(...) の中身までは正規表現で完全に検証しない (CSS パーサ書く羽目になる)。「括弧の中身は CSS パーサに委ねる」 という戦略で実装コスト 1/10。中身が壊れていればブラウザ側がスタイル無視するだけなので、UI が落ちる事故にはならない。
fr は何を計算しているか
grid-template-columns: 200px 1fr 200px を本ツールで描画すると、preview の真ん中のセルがビューポート幅 - 400px をフルに使う。fr は「残った空間を分配する単位」 で、合計 fr 数を分母にする。
container width: 1000px
固定: 200px + 200px = 400px
残り: 600px
1fr = 600px / 1 (合計 fr) = 600px
3 列が 1fr 2fr 1fr なら、1+2+1 = 4 を分母にして 1fr = 150px、2fr = 300px。% との違いは、fr は 固定サイズや gap を差し引いてから分配する こと。% は親要素の幅に対する単純な比率なので、gap や固定列があると合計が 100% を超えてオーバーフロー (もしくは縮む) する。
/* 動かない */
grid-template-columns: 200px 50% 200px; /* 200 + 500 + 200 > 1000 */
/* 動く */
grid-template-columns: 200px 1fr 200px;
fr を理解すると 「サイドバー + 残りを content」 という普遍パターンが 1 行で書ける。本ツールの Holy grail プリセットがまさにこれ。
repeat(auto-fit, minmax(...)) がレスポンシブの正体
repeat(auto-fit, minmax(140px, 1fr)) 一行で:
- セルの 最小幅は 140px
- 余裕があれば
1frで 均等に伸びる - コンテナが狭くなれば 列数を自動で減らす
これがいわゆる "intrinsically responsive grid" の正体。メディアクエリ不要。ブラウザが container width を見て:
-
floor(container_width / 140px)で 最大列数 を計算 - その列数で
1fr分配
つまり 600px のコンテナなら floor(600 / 140) = 4 列、各列 (600 - gap*3) / 4 px。850px なら 6 列。CSS だけでブレークポイント不要のレイアウトが書ける。
auto-fit vs auto-fill の違いも分かる: auto-fill は 空のトラックも数える ので、コンテンツが少ない時に余白の列が確保される。auto-fit は 空のトラックを潰す ので残ったセルが伸びる。本ツールではプリセットで auto-fit を採用 (一般的により欲しい挙動)。
プレビューと出力の一致 — <style> 注入
const liveStyle = document.createElement("style");
liveStyle.id = "live-grid";
document.head.appendChild(liveStyle);
function applyGridCSS() {
liveStyle.textContent = buildGridCSS(state, "#preview");
$("output").textContent = buildGridCSS(state, ".grid");
}
ポイント: プレビュー用と出力用で buildGridCSS() を 2 回呼ぶ。違いは selector だけ (#preview vs .grid)。本体のロジックを 2 回通すので「コピーした CSS と表示が違う」事故が起きない — preview に当たる CSS は、ユーザがコピーするのと文字通り同じ buildGridCSS の出力。
これが「自分のドッグフードを食べる」設計。プレビュー専用の特殊ロジックを書くと、ユーザがコピーしたあとに「あれ、これだと動かない」が発生する。生成器を一本化して両用すれば構造的にズレない。
まとめ
-
gap、stretchのような デフォルト値は出力から省く → コピー結果が idiomatic - トラック構文は 括弧の中身を CSS パーサに任せる ことで実装コスト 1/10、UI クラッシュもなし
-
frは「固定サイズと gap を引いた残りを fr 合計で割って分配」する単位。%と違って overflow しない -
repeat(auto-fit, minmax(N, 1fr))はメディアクエリ不要の責任分界点 — N で最小幅、1frで均等分配、列数はコンテナ幅から自動計算 - プレビューと出力で同じジェネレータを通す ことで「表示とコピーが食い違う」事故を構造的に防ぐ
リポジトリ: https://github.com/sen-ltd/css-grid-builder
このツールは弊社の OSS ポートフォリオ #246 として作成しました。SEN 合同会社(東京)では小さくて切れ味のあるツール群を継続的に公開しています: https://sen.ltd/portfolio/
