スペーシングの設計思想:近接によるグルーピング
グループ間のスペースはグループ内のスペースより明らかに大きくなければ、個々の値が「正しく」ても意図と違う階層として読まれてしまう。
問題
スペーシングは構造を伝えます。読者はページを DOM ツリーとして読むのではなく、目で読みます。そして目は、要素同士の距離の近さでグルーピングを判断します。これはゲシュタルトの「近接の法則」です ―― 近くに置かれた要素は関連していると知覚され、離れて置かれた要素は別のものだと知覚されます。
Design Token Lint や 2層サイズ戦略 のようなトークンシステムや lint ルールは、スペーシングの値が承認済みのスケールから選ばれていることを強制できます。しかし、隣り合う 2 つの値を並べたときに、それが読み取りやすいグルーピングを生むかどうかまでは強制できません。レイアウトが有効なトークンだけで構成されていても、間違った階層として読まれることはあります。失敗の原因は個々の値ではなく、グループ内のギャップとグループ間のギャップの比率にあるからです。
8px、12px、16px、24px、32px というステップを持つスペーシングスケールを考えてみましょう。どの値も「正しい」トークンです。しかし、関連するアイテム間のスペースに 12px を、次のセクションまでのスペースに 16px を使うのは、lint 層では決して検出できない間違いです。16px は 12px のわずか 1.33 倍にすぎず、境界として読み取るには弱すぎる信号です。目には 2 つのグループが 1 つに溶け合って見えてしまいます。
解決方法
グループ間のギャップを、グループ内のギャップより明らかに大きくします ―― 目安としては、おおよそ 2 倍以上です。絶対値そのものよりも、値同士の比率のほうが重要です。2 つのスペーシング値がどちらも有効なトークンであっても、それらがどう関係し合うかによって、まったく異なる構造を表すことになります。
密なスケールでは、このルールを判断に委ねるのではなく機械的に適用しましょう。スケールの中にある ×2 のラダーを見つけます ―― 4/6/8/12/16/24/32/48 というステップを持つスケールには 2 本あり、4→8→16→32 と 6→12→24→48 です ―― そして、どの境界でも両側のギャップを同じラダーから、1 段離して選びます。同じラダーで 1 段上は、構成上ちょうど 2× です。隣り合うスケールステップ(12 と 16、16 と 20)は、別の場所で別の仕事をするためのものであって、1 つのグルーピング境界の両側になるものではありません。スペーシングの使用箇所およそ 3,000 件を実測したある移行では、ギャップが明示的に書かれていた境界は、ペアリングを組み直すだけですべて修正でき、スケールステップは 1 つもリネームも削除もしませんでした。グループ内のリズムが内在的だった境界には別の仕組みが必要でした。次のセクション、および高密度なデスクトップクラス Web アプリのスペーシングアーキテクチャを参照してください。
どちらの列も、3 つの商品グループ内のスペーシングには同じ 12px を使っています。左の列はフッターまでのギャップに 16px を組み合わせています ―― これも有効なトークンですが、12px に近すぎて境界として読み取れず、フッターがリストの 4 つ目のアイテムのように見えてしまいます。右の列は同じ 12px に 32px を組み合わせています ―― フッターは商品グループとは明らかに別のものとして読み取れます。ここではどの値も単体では「間違って」いません。読み取り方を決めているのは比率です。
これらのギャップをどう作るか ―― gap を使うか margin を使うか、どちらをいつ使うか ―― については、gap と margin の使い分け を参照してください。この記事が扱うのは境界のどちら側にどれだけのスペースを置くかという判断であり、あちらの記事が扱うのはどのプロパティでそれを作るかです。
グループ内のギャップが自分では決められないとき
比率のルールは、両方のギャップを自分で制御できることを前提にしています。グループ内のリズムが内在的なこともあります ―― カード内に積み重なったテキストの行の高さ、アイコン + ラベルの行が持つ自然なリズムなど。可読性を損なわずに縮めることはできず、それを比率で上回ろうとすれば(24px の行リズムの 2 倍 = カードごとに 48px)、レイアウトが節約するために存在しているはずのスペースを浪費してしまいます。
そうなったら、近接で戦うのをやめてゲシュタルトの原理を切り替えます ―― 共通領域(common region)を使いましょう。各グループに境界のあるボックス ―― ボーダー、角丸、塗り ―― を与え、ボックス同士は控えめなギャップで並べます。境界のある領域は内部のリズムに関係なく中身をひとまとまりにするので、比率だけで考えれば 48px を要求したはずの 24px の内部行リズムに対しても、カード間 16px のギャップがきれいに読み取れます。
診断方法はこうです。「比率を直す」ことが、テキストの行の高さを縮めるか、すでに十分なギャップをさらに 2 倍にするかのどちらかを意味するなら、そのレイアウトが求めているのは境界のある領域(共通領域)であって、追加のスペーシングではありません。実測されたあるケース ―― カード間のギャップ(約 25px)がカード内の行リズム(約 24px)と一致し、8 枚のカードが 1 つのリストに融合していたタイムライン ―― は、内部のリズムをバイト単位でそのまま残し、ボーダーと角丸と 16px のギャップを加えるだけで解決しました。
セパレーターは、それ自身の側を持たない
HTML は <hr> を「テーマ上の区切り(thematic break)」としてのみ定義しています ―― それ以上のものではありません。線がどのセクションに属するのか、どれだけのスペースを与えるべきか、後に続くものを開くのか前にあるものを閉じるのか、仕様は何も述べていません。要素が提供するのは線であり、意味を与えるのはその周囲のスペーシングです。このため、セパレーターは近接によるグルーピングが最も狂いやすい場所になります。マークアップは自明に見えるのに、レンダリング結果は別のことを語っているからです。
この記事を書くきっかけになった失敗は、まさにこれほど単純なものでした。
壊れた版では、段落は 24px 離れて配置されていますが、<hr> は上下に 8px しか持っていません。「セパレーター」をまたぐ合計のギャップは、関連する段落同士のギャップより小さくなっています ―― そのため、本来分割するはずだった 2 つの段落が、ページ上の他のどのペアよりも強くつながっているように読まれ、線そのものはテキストの装飾のように読まれてしまいます。修正版では段落のリズムはそのままで、<hr> のマージンだけが 48px ―― グループ内ギャップの 2 倍 ―― に広がり、ページはきれいな 2 つのグループに分かれます。上と同じ比率のルールを、区切り線をまたいで適用したものです。
セパレーターの役割を決めてから、スペースを取る
要素それ自体は何の意味も持たないため、最初のステップは値ではなく決定です。このセパレーターを何にしたいのか、ということです。
| 意図する役割 | スペースの形 | 典型的な用途 |
|---|---|---|
| 対称な境界 | 両側に等しく大きなスペース(グループ内ギャップの 2 倍以上) | 文章ブロック間のテーマ上の区切り |
| オープナー ―― 後ろに結び付く | 前に大きなスペース、後に小さなスペース | 次のセクションを導入する線(見出しの罫線など) |
| クローザー ―― 前に結び付く | 前に小さなスペース、後に大きなスペース | ブロックを閉じる線(合計行や署名の罫線など) |
3 つとも正当です。正当でないのは、選ばないことです。2 つのギャップが偶然任せのセパレーター ―― ほぼ等しく小さいか、たまたまマージンの相殺(margin collapsing)によって生じたもの ―― は、脈絡のない線として読まれます。どの役割を選んだとしても、それを支配するのは比率のルールです。分離する側のスペースはグループ内ギャップより明らかに大きく、(もしあれば)結び付ける側は明らかに小さくなければなりません。
border-t + padding-top の罠
オープナーの役割には、よくある不注意な逆転があります。Tailwind の border-t + pt-* パターンは、上部ボーダーと上部パディングを同じ要素 ―― 区切り線の後に来るセクション ―― に適用します。
/* The line sits at the very top of the box.
padding-top lands BELOW the line, not above it. */
.section-next {
border-top: 1px dashed hsl(30, 15%, 55%);
padding-top: 2rem;
}ボーダーはボックスの上端に描画されます。padding-top はボックスの内側、つまりその線より下のスペースです。ボックスの前に何もスペースを与えるものがなければ(そのボックス自体に margin-top がなく、前の要素にも margin-bottom がなければ)、線は前のセクションにぴったりくっついて見え ―― 視覚的にはそちらと結合します ―― 一方でたっぷりとしたパディングが、次のセクションのコンテンツを自分自身の区切り線から遠ざけてしまいます。
壊れた版では、線の上にはほとんどスペースがなく、前の段落と視覚的に結合してしまいます ―― この 2 つは 1 つのグループとして読まれ、「Next section starts here」は自分自身の区切り線の下で切り離されたように浮いて見えます。修正版では、区切り線に大きな margin-top を与えることで前のセクションから線を引き離し、小さな padding-top によって次のセクションのテキストを線のすぐ下に留めています。これで線は本来の姿 ―― 次のセクションの始まり ―― として読まれます。この罠は一般化できます ―― セパレーターの読み取りが狂ったときは、値をいじる前に、目に見えるスペースが物理的に線のどちら側に付いたのかを確認してください。
目を細めて確認するテスト
速くて信頼できるレビューのコツがあります。レイアウトをぼかして見る(あるいは実際に目を細める)ことで、どの要素同士が集まって見えるかを確認するのです。テキストの内容やアイコンの形、正確な色といった細部はぼかすと消えてしまいます。残るのは純粋な空間的グルーピング ―― これは読者の周辺視野や最初のひと目のスキャンが、1 語も読まないうちに拾い上げるのと同じ信号です。
結果を意図したアウトラインと照らし合わせます。区切り線や見出し、ラベルといった境界要素が間違った隣人と一緒に見えるなら、修正すべきはその要素のギャップの比率であって、要素自体のスタイルではありません。区切り線が間違ったセクションに属しているように見えるのは、スペーシングのバグであって、ボーダーの色や線の太さのバグではありません。
ユーザーが操作できるディスプレイスケールを持つアプリ(ディスプレイスケール戦略を参照)では、スケールの両端 ―― 0.75× と 1.5×/2.0× ―― でも目を細めるパスを実行しましょう。端数のあるスケールは、1.0× では見えない丸めとヘアラインのバグをあぶり出します。2px 未満の滲み、半ピクセルのバー、そして乗数に一度も繋がれていないコンポーネントが周囲のすべてから位置ずれしていく現象です。
上のチェックボックスを切り替えてみてください。鮮明な状態では、「24 results」はツールバーの下にある普通のテキストに見えます。ぼかすと、その上の大きなギャップと背景ボックスがないことから、「ツールバーグループの一部ではない」ことがはっきりと読み取れます ―― グルーピングの意図はぼかしても生き残っています。もしそうならなかったら ―― ぼかした形が 1 つの塊に溶け合ってしまったら ―― それはグループ間のギャップを広げるべきだという合図です。
クイックリファレンス
| シナリオ | テクニック |
|---|---|
| リストやツールバー内の関連アイテム | 小さく均一なギャップ(コンテナに gap) |
| 無関係なセクション同士の境界 | グループ内ギャップのおおよそ 2 倍以上のギャップ |
どんなセパレーターも(<hr>、ボーダー、ラベル) | まず役割を決める ―― 対称な境界、オープナー、クローザーのいずれか ―― そのうえで、それを表現するスペーシングを設定する |
| 境界として読ませたいセパレーター | 分離する側のスペースをグループ内ギャップより明らかに大きく ―― 決して小さくしない |
| 出来上がったレイアウトのグルーピングを見直す | ページをぼかす/目を細める。意図したアウトラインとの一致を確認する |
| スペーシング値を階層バグの観点で監査する | 各値を単体で見るのではなく、隣り合うギャップの比率を比較する |
AIがよくやるミス
「有効なトークン」を「正しい使い方」だと思い込む。 スペーシングスケールは許可される値を制約するだけで、どの値がどの境界に属するかまでは制約しません。隣接するスケールのステップ(例:12px と 16px)をグループ内・グループ間のスペーシングに使うと、規約上は合法でも読み取れないレイアウトになります。
セパレーターに、周囲のコンテンツのリズムより小さいスペースしか与えない。
<hr>をまたぐギャップが段落同士のギャップより小さいと、「セパレーター」はその両側を、普通の段落よりも強く関連しているように読ませてしまいます ―― この記事を書くきっかけになった、まさにその失敗です。区切り線に続くセクションに
margin-topを追加せずborder-t+pt-*を適用する。 パディングは線の上ではなく下に付くため、オープナーのつもりが、知らないうちに前のセクションのクローザーになってしまいます。レンダリング結果を見るのではなく、デザインシステムと照合してスペーシングをレビューする。 lint パスやトークン監査では近接グルーピングのバグは検出できません ―― 視覚的な確認(できれば目を細めるテスト)だけがそれを見つけられます。
単一のギャップだけを変更してスペーシングの不満を「解決」しようとする。 境界の読み取りが間違っているなら、修正すべきは隣り合う 2 つのギャップの比率であり、1 つの値への場当たり的な変更ではありません。
区切り要素は周囲のスペースに関係なく自明だと思い込む。 HTML は
<hr>をテーマ上の区切りとしてのみ定義しており、それ自体では、どのセクションに属するかを何も伝えません。その働きをするのは周囲のスペーシングであり、どの役割を担うか(境界、オープナー、クローザー)は要素ではなく書き手の決定です。
使い分け
比率のルールを適用する
レイアウトに複数階層のグルーピングがある場合は常に ―― カード内のアイテムのリスト、ページセクション内のカード、ページ内のセクションなど。各階層の「間」のギャップが、その 1 つ下の階層の「内」のギャップより明らかに大きいことを確認します。
目を細めるテストを実行する
レイアウトを構築またはリスタイルした後、公開する前に。トークン/lint 監査では表面化しないグルーピングのバグを、ツールなしで素早く見つける方法です。なぜなら、これは値そのものではなく値同士の関係を確認するからです。
特に border-t + pt-* パターンに注意する
区切り線や上部ボーダーが、専用のセパレーター要素ではなく、新しいセクションを開始する要素に適用されている場合は常に。その線のどちら側に実際に目に見えるスペースが付いているかを確認してください。