TL;DR
- 社内 AI ワークショップ用の3つのデモページ(お菓子店・美容室・飲食店)を 12 列モジュラーグリッドに乗せ替えた
- 最初はヒーローだけ subgrid で「整数比だから乗ってるはず」と目視で済ませた → ユーザーから「写真の高さが合ってない」と突き返された
- 検証ハーネスを走らせたら
product-rowで 16px、info-gridで 20px ズレていた。gapをvar(--gutter)に揃え忘れていた - 根本原因は3つ重なっていた:
grid-template-columnsの1fr問題・box-sizingが content-box のまま・translateYの装飾が残骸として効いていた - 最終的に 3 ページ全要素 0.03px 以下まで詰めて、固定ナビ追加とサイドバー削除も同じ流れで整えた
きっかけ: 「グリッドのスキル使ってちょっと整えて」
朝、社内 AI ワークショップ用のデモページ3本のグリッドレイアウトを整える依頼が飛んできた。お菓子店・美容室・飲食店の3業種で、それぞれ独自のテーマ色とフォントを当ててある。
このお菓子のページとかデモページ作ってるんですけど、あと美容室と飲食店の3つですね。今日追加したグリッドデザインのスキルを使って、いい感じにグリッドレイアウトで整えてくれませんか。
今日追加した muller-brockmann-grid-systems スキルを当てる初の実戦。色やフォントは各ページの世界観として残し、骨格だけ 12 列モジュラーグリッドに乗せ替える方針で進めることにした。
序盤: 既存の grid-system.css に乗っかる
ディレクトリを見ると、すでに grid-system.css と useGridOverlay.ts が用意されていた。誰も使っていない状態のスキャフォールド。自分で作りかけた tsukumi-grid.css を削除して、既存資産に乗ることにした。
最初の作業はこの順番で進めた。
nuxt.config.tsにcss:を追加してgrid-system.cssを読み込ませる- 各ページのルート要素に
.gs-wrap .gs-gridを当てる - ヒーローセクションだけ subgrid 化して、タイトルを col 1-5、写真を col 6-12 に並べる
ここまではスムーズに動いた。グリッドオーバーレイを当てるとコラム1-5にタイトル、コラム6-12に写真がきれいに乗る。「いい感じだな」と思いながら、3ページとも同じ手順で進めて報告を投げた。
ユーザーからの差し戻し: 「ちゃんと縦とか横とか揃えてくれていますか?」
報告を投げた直後、ユーザーから返ってきた。
ごめん、待って。ちゃんと縦とか横とか揃えてくれていますか?スキルの検証ハーネス使って、ちゃんと確認してくれていますかね。
ここで手が止まった。正直に言うとヒーローセクションだけ subgrid で 0px 一致を確認していて、それ以外(ギフトセクション、商品行、料理行、店舗情報、FAQ など)は「整数比で目視で乗ってるはず」というレベルで放置していた。スキルが用意している verify ハーネスは一度も回していなかった。
謝罪して、検証ハーネスを全セクションで走らせることにした。
verify を回したら 16px と 20px ズレていた
検証スクリプトで全主要グリッド要素の左右と基準コラム line の差分を測る。結果はすぐ出た。
- お菓子店ページの
.product-rowが 16px ズレ - 美容室ページの
.info-gridが 20px ズレ
原因は単純で、内部の gap が var(--gutter)(24px)になっていなかった。6:6 の対称分割でも、内部 gap が外側の 24px と違うとコラム line と一致しない。整数比で乗っているように見えても、ピクセル単位では合っていなかった。
ここで作業を切り替えた。
- 非対称分割(7:5 など)は素直に subgrid 化する
- 対称分割は gap を
var(--gutter)に揃える - フルブリードのセクションは内側ラッパーを追加して、その中でグリッドを継続する
写真の高さが揃わない: 「これなんで写真の高さが合ってないんですか?」
verify が通ったと報告したら、ユーザーからスクリーンショットが飛んできた。
ごめん、だからマジでびっくりするんですけど、これなんで写真の高さが合ってないんですか?
美容室ページのスタイルカードが上下にガタついていた。verify は左右の left と right しか測っていなかったから、縦のズレを見落としていた。
ここでバグが3つ重なっていることが判明した。
原因1: translateY の装飾が残っていた
.style-card:nth-child(even) { transform: translateY(...) } で偶数番目のカードを意図的に下にずらすデザインが残っていた。グリッドに乗せたあと、これを消し忘れていた。さらに .style-card.reveal { translate: -28px 0 } の横スライドインが viewport 外で残っていて、初期状態のままだとカードが 28px 横にズレた状態で居座っていた。
縦スライドに変えて、is-visible 前でも横ズレが起きないように修正。
原因2: grid-template-columns: repeat(12, 1fr) の 1fr が拡張されていた
これが一番ハマった。1fr は内部的に minmax(auto, 1fr) として扱われる。子要素の min-content が大きいと、その列だけ拡張される。「スタイルと店内」の h2 が2段に折れていたのもこの影響で、box が狭くなって h2 の min-content と衝突していた。
minmax(0, 1fr) に変えて、列幅が子要素に引っ張られないように固定。
原因3: box-sizing が content-box のまま
最大の地雷。プロジェクト全体に * { box-sizing: border-box } が当たっていなかった。.gs-wrap の中の要素はすべて content-box で計算され、padding を足すと box width が想定より大きくなる。.steps-section の box が 1703px になっていたのはこれが原因。
.gs-wrap 配下を border-box に統一して、ようやく計算と実測が一致するようになった。
修正後に再 verify を走らせて、お菓子店・美容室・飲食店すべて 0.03px 以下(事実上 0px)まで詰まった。
縦に並んでしまう: subgrid の継承が切れていた
ユーザーから次の指摘が飛んできた。
ここなんですが、レイアウトが普通に縦に並んでしまい、見づらいですね。
お菓子店ページを開くと、商品カードが横並びにならず縦1列で表示されていた。subgrid を指定しているのに2列扱いになっている。
詳細を追うと、.product-list の親 .menu-section が block レイアウトだった。subgrid は親が grid container でないと継承できない。途中に block 要素が挟まると、そこで subgrid のチェーンが切れて単独 grid 扱いになる。
修正は素直だった。
.menu-sectionを grid 化(.gs-gridを当てる)- 美容室ページの
.info-section、飲食店ページの.access-sectionも同じ問題があったので、まとめて grid 化 - 逆に
.access-innerは自分が独立 12 列 grid container だったため、subgrid 指定を追加していたのが裏目に出ていた。subgrid 指定を消して元に戻す
3 ページとも再 verify を通して、横並びが復活した。
固定ナビとサイドバー削除
最後に2つ整えた。
各ページに固定ナビを追加
3 ページとも、上部に固定ナビを追加した。お菓子店ページなら「ギフト / 商品 / 店舗情報 / FAQ」のように、ページ内アンカーで section にジャンプできるようにした。scroll-margin-top を 88px ほど確保して、ナビ下にちゃんと余白が残るように調整。
dev server が途中で落ちて再起動したり、リビルドで数十秒待たされたりしたが、3 ページとも #gift などに top=87.875 で固定されてスクロールするのを確認できた。
index ページのサイドバー TOC を削除
index ページ(/tsukumi-ai-workshop 自体)にはスクショ付きで「ここはいらない」と指摘されたサイドバーの目次があった。
スクリーンショット 今回ここはいらないですね。
サイドバー TOC を消して、代わりに上部中央に他ページと同じ固定ナビ(チラシ文面 / 当日の進行 / 成果物 / プロンプト / 運用)を追加。メインコンテンツを 12 列フル幅に拡張して、メトリクスカードを col 1-3 / 4-6 / 7-9 / 10-12 でちょうど 4 等分に並ぶように組み直した。
学び
verify ハーネスを最初から回す
直接の敗因は、スキルが用意している verify ハーネスを最初から回さず「目視と理屈で済ませた」こと。ヒーローだけ subgrid で 0px 一致を確認した時点で「他も整数比だから乗ってるはず」と判断してしまった。実際は 16px と 20px ズレていた。
「整数比で計算上は乗っているはず」は、gap の値が違うだけで一発で破綻する。スキル側に検証ハーネスが用意されているなら、最初から全要素で走らせるのが正解だった。
1fr ではなく minmax(0, 1fr) を使う
grid-template-columns: repeat(12, 1fr) は内部的に minmax(auto, 1fr) 扱いになり、子要素の min-content で列幅が拡張される。Müller-Brockmann グリッドのように 12 列を厳密に守りたいときは、最初から minmax(0, 1fr) で書くのが安全。
box-sizing は最初に統一する
プロジェクトに * { box-sizing: border-box } が当たっていない状態でグリッドに乗せると、padding 付きの要素が想定より大きくなって計算が合わなくなる。グリッドシステムを導入する最初のステップで、ラッパー配下を border-box に統一しておくと後段の verify で詰まらない。
subgrid の継承は親の display で切れる
subgrid を効かせたいときは、上位の grid container から子孫まで全部 grid container でつないでおく必要がある。途中に block の section を挟むとそこで継承が切れ、子の subgrid 指定は単独 grid container として扱われる。今回の .menu-section → .product-list の縦並びはこれが原因だった。
装飾の translate は viewport 外で残ることがある
スクロール演出で translate: -28px 0 のような初期状態を持たせていると、is-visible 前のカードが横にズレた状態で居座る。verify が「初期状態」を測定するときに、装飾が verify を汚すケースがある。グリッド規律を厳密に守るページでは、初期状態が viewport 内に入らない設計にしておくか、縦方向のスライドに切り替える。
残タスク
- 同じグリッドシステムを
/tsukumi-ai-workshop配下の他のページにも展開する - verify ハーネスを CI で走らせて、リグレッションを自動検知できるようにする
-
minmax(0, 1fr)をgrid-system.cssのデフォルトに昇格させる