Vuetify 4 が 2026 年 2 月に出ました。既存プロジェクトを上げたところ、引っかかった順に「テーマ」「フォント」「グリッドの余白」の 3 つでした。最初の 2 つは軽めです。本丸は最後のグリッドで、いきなり全テンプレートを直すのではなく、まず暫定で止血して動く状態を維持し、そのあとでページごとに恒久対応しました。やったことをメモしておきます。

まず軽いところから

テーマのデフォルトが変わっていた

v4 でデフォルトテーマが light から system(ブラウザ/OS 設定に追従)に変わりました。自分のブラウザがダークモードだったので、上げた瞬間に一部の背景が黒くなって面食らいました。

plugins/vuetify.ts で明示的に固定して解決しました。

theme: {
  defaultTheme: 'light',
  // ...
}

ここはアプリの要件として「ライト固定」と決めているだけなので、迷わず一行です。

フォントは自動修正で直せた

MD3 タイポグラフィへの寄せで、クラス名やサイズが変わっています。ここは手で追わず、eslint-plugin-vuetify の自動修正に任せました。フォントや一部デザインは自動修正で直せました。

% yarn eslint --fix

一部フォントサイズが大きくなりましたが、絶対値にこだわりがある箇所ではなく相対サイズが動いただけだったので、直さずそのまま流れに乗りました。「変わったから戻す」を反射でやらないのも判断のうちで、ここで時間を使わない方が全体は早いです。

本丸:グリッドの余白(負マージン → gap)

何が変わったのか

ひとことで言うと「自分で計算して相殺していた余白を、ブラウザネイティブの gap に丸投げした」変更です。

v3 までは flexbox を使いつつ、隙間を列の padding で作り、外周に出る分を行の負マージンで打ち消してコンテナ端に揃えていました。

/* v3 の考え方(ガター 24px) */
.v-row          { margin: -12px; }   /* 外周を相殺 */
.v-row > .v-col { padding: 12px; }    /* 隣接で計 24px */

v4 は flexbox は維持したまま、行に CSS の gap を当てます。gap はアイテム間だけに効くので、外周相殺の負マージンも列ごとの padding も要らなくなりました。操作系も densitydefault / comfortable / compact)と、任意値を入れられる gap プロパティに整理されています。

方式そのものは素直です。問題は、旧機構に乗っかって組んだ既存のレイアウトが黙ってズレることでした。これが量としては一番大きかったです。

暫定対応:v3 の挙動を override で再現する

ありがたいことに v4 は breaking change ごとに revert スニペットを用意していて、グリッドにも公式の Grid Legacy Mode があります。@layer vuetify-overrides で v3 の負マージン+列 padding を再現すれば、テンプレートを一切触らずに見た目を据え置けます。

スニペット自体は AI に書いてもらいました。layouts/default.vue に置いたのが以下です。

/* v3 の負マージン方式を再現する暫定対応(本格移行時に削除) */
@layer vuetify-overrides {
  .v-row {
    margin: -12px;
    /* gap だけでなく変数も 0 に(col 幅計算が使う。残すと右に余白) */
    --v-col-gap-x: 0px;
    --v-col-gap-y: 0px;
  }
  .v-row + .v-row { margin-top: 12px; }          /* 2 つ目以降の行だけ上マージン */
  .v-col, [class*='v-col-'] { padding: 12px; }   /* v3 の col padding 再現 */
}

ここのハマりどころが --v-col-gap-x / --v-col-gap-y でした。gap を 0 にするだけでは足りません。v4 の v-col は幅計算にこの変数を使っているので、変数自体を 0 にしないと列幅が gap の分だけ狭まり、行の右側に余白が残ります。負マージンを当てているのに右がズレる、で一度悩んだ末にここでした。

もう一つ、v4 は CSS reset が削られていて、見出しにブラウザ既定の margin が付くようになっていました。これも v3 相当に戻します。

/* reset 削減で h1-h6 に付くブラウザ既定 margin を戻す(utility で上書き可) */
@layer vuetify-components {
  h1, h2, h3, h4, h5, h6 { margin: 0; }
}

レイヤーの置き場所が肝で、グリッド復元は vuetify-overrides(density 既定より上)、見出し reset は vuetify-components(utility より下)に分けています。こうすると暫定対応で全体を据え置きつつ、個別の mt- / pa- での微調整は殺さずに残せます。止血しても身動きが取れる状態にしておくのが狙いです。

恒久対応:暫定を剥がして、ページごとに直す

落ち着いたところで上記の暫定対応を削除し、各ページを開いて本来の形に直していきました。出てきたパターンはおおむね次の通りです。

  • 見出しの margin を明示で殺す。 CSS reset 削減のぶん。<h4><h4 class="my-0">
  • v-card-text が周りのマージンを増やしていた。 グリッドを入れるだけの用途なら外す。v-card-text は本文テキスト用の padding コンテナなので、レイアウトの器として使うと余白が乗ってきます。
  • gap="0" の要否が行の位置で変わる。 1 つ目の行には要らないが、2 つ目以降は上マージンが増えるので必要になります。ただし 2 つ目でも付けなくて問題ないケースもあって、結局は見て決めるしかありませんでした。
  • utility の調整量そのものが変わる。 gap 機構が消えたぶん、隙間を作る側の数値を入れ直す形になります。pr-0 でよかったものが `pr-3` になる、という逆転が起きます。

具体的には、こんな形で直していきました。

見出し: reset 削減ぶんを my-0 で消す

- <h4≷{{ title }}&tl;/h4≷
+ <h4 class="my-0"≷{{ title }}</h4≷

カード: v-card-text を外し、gap="0" を付け、列の余白量を入れ直す

<v-card>
- <v-card-text>
    <v-row>
-     <v-col cols="auto" class="pr-0">{{ label }}</v-col>
+     <v-col cols="auto" class="pr-3">{{ label }}</v-col>
      <v-col>{{ value }}</v-col>
    </v-row>
- </v-card-text>
</v-card>

v-card-textclass="pa-0" で padding を殺すだけでも対処できますが、
今回は削除して構造をシンプルにする方を選びました。

- <v-card-text>
+ <v-card-text class="pa-0">

そもそもデザイン崩れは、いまの AI からは見えません。「動くけれど、なんとなく気になる」という人間の感覚の領域なので、ここは自分で画面を見て直すしかありませんでした。とはいえ、gap="0" の要否や utility の量といった対応方針さえ箇所ごとに決まれば、あとは同じ流れで当てていくだけです。数は多くても機械的に処理でき、思ったほど時間は掛かりませんでした。

まとめ

  • gap 方式自体は素直で、相殺ロジックが消えたぶん予測がつきやすいです。方向性は歓迎しています。
  • 移行で人間が要るのは「デザイン崩れに気づくこと」と「箇所ごとにどう直すか決めること」の 2 点でした。前者はいまの AI には見えず、後者も最終的には目で見る判断です。
  • 逆に、方針さえ決まればあとは機械的なので、箇所が多くても時間は掛かりませんでした。
  • revert スニペットで段階移行できる設計のおかげで、止血 → 範囲調査 → 手作業、と素直に分解できました。--v-col-gap-x のような細かい罠はありましたが、踏んで直すだけのことです。

メジャーアップというと身構えがちですが、やったことを並べれば、変わった箇所を洗い、固定できるものは固定し、自動化できるものは任せ、判断が要るところだけ手で直しただけでした。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です