🧑‍🤝‍🧑

「余白はmarginではなくSpacerでつける」が自分には合わなかった話

2022/05/04に公開
1

Reactにおいて、余白を指定するためにmarginを使用せずにSpacerコンポーネントを使用するという手法を知りました。
Spacerという、中身を持たず高さ・幅のみを持つコンポーネントを定義して余白を作る考え方です。

https://zenn.dev/seya/articles/09545c7503baa4
https://zenn.dev/def_yuisato/articles/replace-margin-with-spacer-in-react
https://javascript.plainenglish.io/stop-using-margin-use-spacer-component-instead-953d9b2dbacc

Spacerを使った例
<Header />
<Spacer y={2} />
<Section1 />
<Spacer y={1} />
<Section2 />
<Spacer y={2} />
<Footer />

この手法を知って一度取り入れてみたのですが、自分には合わなかったので考えを整理しておきたいと思います。

はじめに

  • 勘違いされてしまうと嫌なので書いておきますが、上に挙げた記事を否定・批判する意図は一切ありません(自分自身、はじめはすごくいい考えだと思いました)。違った視点での見解を提供できたらと思いますので、どの手法にすべきかの判断材料にしていただければと思います。
  • 「プロジェクト性質」や「プロジェクトメンバーのスキルセット」、そして「好み」などの個別の状況によっても適切な手段は異なると思います。タイトルの通り、あくまで「自分には」という話になります。

想定読者

  • marginを使う方針にするか、Spacerを使う方針にするか、迷っている人。
  • Spacerを使った実装をやってみているが、しっくり来てない人。

前提の整理

余白は「関係性」によって規定される

デザインの考え方として、要素間の余白がどういうルールで規定されるかと言ったら、 「要素と要素の関係性」 だと思います。
関連する要素同士は小さいマージンで、逆に関係ない要素同士は大きいマージンで並びますし、並列する要素は同じマージンで並びます。

よって、「ボタンの隣がなんであろうと必ず10px空ける」ということにはならず、
「テキストとボタンが並んだら20px」「ボタンとボタンが並んだら10px」などのような指定が必要になります。

コンポーネント自体がマージンを持つべきではない

前の項で確認した通り、「余白は関係性によって規定される」ので、コンポーネントそれ自身がマージンを持つべきではありません。
要素と要素がどう並ぶかを知る、コンポーネントを使う側(=親)がマージンをつけるべきです。

ただし、例外はあり、特定のコンポーネント内で使用されることがわかっている場合、つまり前後に来る要素がある程度決まっている場合には、コンポーネントにマージンを持たせても良いと思っています。
これについては後で説明します。

margin/Spacer/grid の何を使うかは、実装上の都合の問題である

余白の指定のために、margin/Spacer/grid(gap) の何を使うかは、
「余白は要素と要素の関係性によって規定されるという思想」を実装に落とし込むのに、 「どの方法だと都合がいいか」 という問題でしかないと思います。
なので、「〇〇であるべき」という話ではなく、「〇〇だと都合よく実装できる」という話になります。

ここから先は 「何を使うと都合がいいか?」 という話で、
私は「関係性で余白を定義する」には、Spacerよりも、marginのほうが適していると考えています。

Spacerの何が合わなかったか

条件分岐があると、関係性で記述することが困難になる

条件分岐が複数あり、連続する要素が変わる場合に、Spacerの数値を要素の関係性で定義することが難しくなります。

<Title />
{hasText && (
    <>
        <Spacer y={10} />
        <Text />
    </>
)}
{hasNote && (
    <>
        {/* Titleと連続するなら10、Textと連続するなら5としたい */}
        <Spacer y={hasText ? 5 : 10} />
        <Note />
    </>
)}

「通常のコンポーネント」と「Spacerコンポーネント」の区別がつきにくくなる

このへんは感覚的な話になってくるのですが、やりやすさは作業効率に直結する要素ですので軽視もできないと思っています。

「通常の(中身・機能を持つ)コンポーネント」と「Spacerコンポーネント」が並んでいると、
ソースを編集するときや、ブラウザでDevtoolを参照するときなどに、一見して区別することが難しくなりました。

// 中身のあるコンポーネントがわかりにくい
<Header />
<Spacer />
<Title />
<Spacer />
<Text />
<Spacer />
<Footer />

上記はpropsもなにもつけていない極端な例ですが、
propsや中のテキストが入ったとしてもパッと見で見分けにくいのは確かです。

スタイル調整のときにCSSではなくコンポーネントをいじる必要がある

感覚として、「表示する要素・機能をいじるぞ!」ってときと、「CSSの数値調整をするぞ!」というときって、なんとなくモードが違うんですよね。
「ここの余白はpaddingだからCSSで編集」、「ここはSpacerだからコンポーネントの値を編集しなきゃ」と行ったり来たりすると、いちいち頭の切り替えが必要になりスムーズに進まない感覚がありました。

通常のCSSの考え方と異なるので、慣れていない人が触りにくい

特にReactに不慣れなメンバーがCSS部分だけ参加するということが難しくなります。

関係性で定義するにはmarginでがいいと思う

margin-top, margin-leftであれば、関係性で定義ができる

margin、中でもmargin-top, margin-leftを使って、後に登場する要素に対して余白を指定していけば、隣接兄弟結合子 (+) が使用できるので、関係性によって余白が定義できます。[1]

.note {
    .title + & {
        margin-top: 20px;
    }
    .text + & {
        margin-top: 10px;
    }
}

念のため補足しておくと、これは「デザイン思想的に後ろの要素が余白を持つべき」ということではなく、
「後ろの要素につけることにしておくと、隣接兄弟結合子(+) が使えて実装的に都合がいい」 ということに過ぎません。

具体例

例1 コンポーネントにclassNameを渡す

先程挙げたスタイルを当てるため、親側から「marginを指定するためのclassName」をつけます。
コンポーネント自身のスタイルにはmarginを指定しません。
「marginを使うこと」が問題なのではなく、「コンポーネント自身がmarginを持つこと」が問題なので、親がmarginを指定すれば問題ありません。

<Title className="title" />
{hasText && <Text className="text" />}
{hasNote && <Note className="note" />}

例2 コンポーネントにclassNameを渡すことを許容しない場合、ラッパーをつける

例1とほぼ同じです。プロジェクトのルールとしてコンポーネントにclassNameを渡さないことにしている場合、こちらの方法を取ることになると思います。

<div className="title"><Title /></div>
{hasText && <div className="text"><Text /></div>}
{hasNote && <div className="note"><Note /></div>}

例3 特定の要素内で使うことがわかっているコンポーネントには、コンポーネント自体にマージンを設定する

基本的にはコンポーネントにはマージンをつけないべき、としていましたが、
使用される状況が決まっている・隣接する要素が決まっている場合は、コンポーネント自体にマージンを設定しても良いと思います。
ただし、その場合も関係性で定義するようにします。

<Modal>
    <ModalTitle />
    <ModalText />
    <ModalNote />
</Modal>
const ModalNote = () => (
    <p className="ModalNote">
        ...
    </p>
)
.ModalNote {
    // 何かしらと隣り合った場合にマージンをつける
    * + & {
        margin-top: 10px;
    }
    // 必要に応じ、隣り合う要素によって数値を変える
    .ModalTitle + & {
        margin-top: 5px;
    }
}

gridで書ける余白はgridで書く

基本的に「marginで実装する」という話をしてきましたが、
gridで書けるものについてはgridで書くほうが楽に書ける場合が多いです。

主なパターンは、「並列する要素が並ぶとき」と「縦横に要素を並べたいとき」です。

並列する要素が並ぶとき

<ul className="list">
    <li></li>
    <li></li>
    <li></li>
    <li></li>
</ul>
.list {
    display: grid;
    row-gap: 5px;
}

縦横に並ぶとき

<div className="grid">
    <Title className="title" />
    <Button className="button" />
    <Text className="text" />
</div>
.grid {
    display: grid;
    grid-template-areas: 
        "title button"
        "text  text  ";
    gap: 5px 10px;
}

.title {
    grid-area: title;
}
.button {
    grid-area: button;
}
.text {
    grid-area: text;
}

まとめ

  • 余白は関係性で規定される。
  • margin/Spacer/grid などの手段がある中で、実装する際に都合のいい方法を使えばいい。
  • 自分はmarginを使うのが都合がいいと思う。
    • 隣接兄弟結合子 (+) で、関係性を記述できるから。
  • 以下の場合は、gridで書くと都合がいい。
    • 並列する要素
    • 縦横に並ぶ要素
脚注
  1. :has() セレクタを使えば .text:has(+ .note) のように先に登場する要素に指定することもできますが、2022年現在実装されているブラウザがSafariだけという状況なので、実質的にこの方法は使えません。 https://developer.mozilla.org/ja/docs/Web/CSS/:has ↩︎

GitHubで編集を提案

Discussion

やまじゅんやまじゅん

xsystem使うと、コンポーネントにサイズを持たせずに、レイアウトレイヤーで調整できますよ