2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSS Grid テンプレートをビジュアル編集するツールを実装 — fr / minmax / repeat が実際に何を計算しているか

2
Posted at

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

Screenshot

ジェネレータと 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-gapgap: に畳む

ナイーブに 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 = 150px2fr = 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 を見て:

  1. floor(container_width / 140px)最大列数 を計算
  2. その列数で 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 の出力。

これが「自分のドッグフードを食べる」設計。プレビュー専用の特殊ロジックを書くと、ユーザがコピーしたあとに「あれ、これだと動かない」が発生する。生成器を一本化して両用すれば構造的にズレない。

まとめ

  • gapstretch のような デフォルト値は出力から省く → コピー結果が 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/

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?