Reactで余白をどうスタイリングするか

4 min read読了の目安(約4200字 3

最近余白の実装について見直す機会があったので、考えをまとめてみました。

TL;DR

  • Grid なら grid-gap
  • flexbox なら flex-gap にしたい(が、safari が対応してないので記事執筆時点では使えない)
  • 適切な padding を指定する
  • 複数の同一のマージンには Stack、それ以外には Spacer コンポーネント

前提: 子コンポーネントは親コンポーネントの"レイアウトのスタイル"を知ってはならない

まず前提として「子コンポーネントは親コンポーネントの"レイアウトのスタイル"を知ってはならない」です。

(太古に書いた記事から具体例を引用)

https://qiita.com/seya/items/8814e905693f00cdade2#スタイルクローズドの原則

例えば、こんな感じのアイコンが複数並べたコンポーネントが存在するとします。

アイコンの間にはmarginが等間隔でありますね。
このmarginをアイコンコンポーネント内で定義していたとしましょう。

.icon {
  ...
  margin-right: 15px;  
}

さて、他のページでこのアイコンを使いたいとなったとします。そして今回はアイコンの右に対してmarginが100px必要なレイアウトだとします。この時どこにどうmarginのスタイルを記述して実現すると良いでしょう?

解決方法は色々ありますが、どんな選択肢であろうとこのアイコンコンポーネント内のmarginを気にしなくてはならないと思います。そしてそんな解決策が積み重なっていくとどんどん汚いCSSが増えていくことは容易に想像できると思います。

このため子のコンポーネントは親にどんなレイアウトで使われるのかについてのスタイルは記述するべきではありません。

適切な padding なら持たせていい

念の為補足ですが、同じ余白でも適切な padding であれば複数の文脈で使われうるコンポーネントでも持たせてもいいと考えています。当たり判定のためにボタンのコンポーネントを見た目以上に大きくするなどの工夫を凝らすこともあるでしょう。

余白は誰の持ち物なのか、というのを意識して正しい判断ができていれば大丈夫です。願わくばデザインの段階で padding とか意識して設定してくれていたら最高ですね。

margin は誰の持ち物?

「余白は誰の持ち物なのか、というのを意識して正しい判断ができていれば大丈夫」と述べましたが、では padding ではなく、要素間の間の大きさを指定する margin は誰の持ち物なのでしょうか?

私は「親」もしくは「誰の持ち物でもない」のどちらかだと考えています。

親の場合

分かりやすいのが Grid layout です。CSS の Grid では親である Container がレイアウトに関するスタイルを記述します。余白は grid-gap で指定したりします。

.grid {
	display: grid;
	grid-gap: 1rem;
}

誰の持ち物でもない場合

レイアウトに関して親が構造を規定しない場合、 margin は誰の持ち物でもなく、独立した存在であると考えることができます。(この場合も親コンポーネント内で並べるので厳密には親の責務なんですけど)

これはデザイナーが余白を作る時の考え方とも一致しているはずです。ある要素とある要素の間に余白を作る時は「余白はこっちに紐づいているな」などとは考えず「これらは同じ概念のものだから余白は小さめ、これらは全然違うからもっと離そう」など、それらの要素の "関係性" に基づいて決めているはずです。

なので、 margin はどちらにも属さず、 margin を持つという一点のみに責務を持った存在であると考えられます。これを実現するための Spacer については後述します。

ちなみに「margin-top と margin-bottom どちらを使うべきか」のような議論が度々起こりますが、この考え方で解消できると思っており、私の答えは「margin は独立しているのでそれ単体で扱うべできであり、どちらでもない」です。

実装方法を考える

では前提を確認したところで具体的な実装方法を考えていきます。

親 - Grid の場合

先ほども例として出しましたが、 Grid Layout の場合は grid-gap を用いて整えます。(grid- をつけないと Safari で動きません)

https://developer.mozilla.org/ja/docs/Web/CSS/gap

縦方向と横方向にそれぞれ指定することができます。

同じ行内や列内に違う幅を設定したくなった場合はうまいことネストしてなんとかさせましょう。

親 - flexbox の場合

gap を使いたいのですが、この記事執筆時点では Safari が対応していないので採用は難しいでしょう。早く実装してくれ!!

https://developer.mozilla.org/ja/docs/Web/CSS/gap
https://caniuse.com/flexbox-gap

親 - 子要素間のマージンが同一の場合

ul ol などのリスト要素を使う場合があてはまります。

これに関してはよくある Stack コンポーネントを使います。
例として Chakra UI の Stack を見てみます。

https://chakra-ui.com/docs/layout/stack

このように Stack と言うコンポーネントの spacing を設定するとその値が要素間の margin として適用されます。

自作する場合は色々方法あるとは思いますが、 Chakra UI では children を map してそれぞれ StackItem と言うコンポーネントでラップして余白のスタイルがあたるようにしているみたいです。

この Stack に関して、たまたま同じ margin の値が複数ある時とかもこの方法使ってもいいかもしれません。(ただ、概念的にはおかしいので、将来的に新しい要素が追加されて違う margin を設定したくなると分解せざるをえなくなったりはすると思います。なので、その場合は Spacer 使った方が丸いかなと)

誰の持ち物でもない場合

最後に誰の持ち物でもない場合ですが、ここでは Spacer コンポーネントを作る方向で考えてみます。

自作する場合は次のように 大きさ(size)方向(axis) が指定できる形で作るといいと思います。
(https://www.joshwcomeau.com/react/modern-spacer-gif/ より引用)

import React from 'react';

const Spacer = ({
  size,
  axis,
  style = {},
  ...delegated,
}) => {
  const width = axis === 'vertical' ? 1 : size;
  const height = axis === 'horizontal' ? 1 : size;
  return (
    <span
      style={{
        display: 'block',
        width,
        minWidth: width,
        height,
        minHeight: height,
        ...style,
      }}
      {...delegated}
    />
  );
};
export default Spacer;

必要な props は size だけになるのでシンプルに書けます。

// 16px × 16px になる
<Spacer size={16} />

もし、指定したい方向以外の Spacer の幅が邪魔になる場合は axis の方向を指定してあげて、必要最小限の形にします。

// 32px × 1px になる
<Spacer axis="horizontal" size={32} />

おわりに

Spacer は初見では気持ち悪く感じるとは思いますが、理にかなった存在であると個人的には思います。Stack と Spacer を駆使していい感じの余白スタイリングをしていきましょう💪

参考記事

https://www.joshwcomeau.com/react/modern-spacer-gif/
https://mxstbr.com/thoughts/margin/