<Stack /> コンポーネントで学ぶ CSS の owl selector

Yuya, web developmentCSS
Back

フロントエンジニアしていたら絶対に遭遇したであろう、二つ以上のブロック要素の間隔どうやって開けるか問題。

例えば、以下のコードがあるとします。

<div>ブロック1</div>
<div>ブロック2</div>

見た目はこんな感じ

ブロック1

ブロック2

まぁ想像通りだと思います。でもこんなんでオッケー出すデザイナーがいるわけありません。

大体は、間にスペースを開けて、それぞれがちゃんとはっきりさせたいはずです。

まぁこんな感じ

ブロック1

ブロック2

何が言いたいかっていうと、単純にブロックが二つなら stylemargin-top をあててしまえば解決ですが、これが例えば三つ以上の場合は皆さんどうしてますか?

ブロック1

ブロック2

ブロック3

こういう場合。

僕がみてきた中では、この二つが主流でした。

一つ一つ、何があまりイケてないのか解説していきましょう。

まず、

block という className を作って、margin-top をあてる。でも first-child にはマージンあてたくないので、not:first-child って書いて回避

よくあるやつですね。何が微妙かって、毎回これを書く必要があるから超めんどくさい。こいつもユーティリティークラスみたいなのにしてしまえばいいとかあるのですが、マージンの値を変える場合とか、その度に同じようなクラスを作りざるを得ません。

global css に m-t-m みたいなユーティリティークラスを作る。そして、それぞれのブロックに className として付与する。

こっちの方がまだマシかな〜って思います。デザインシステムを導入してなくてもしていてもこのように抽象化することによって、アプリコードを書いてる側としては感覚でピクセルバリューを決めるとかいう荒技をすることもなくなるので。

それでもまだめんどくさいですよね。ブロックが10個ある場合、それぞれに className として付与しなければなりません。

できれば何個あるかとか考えたくもないし、マニュアル作業をなるべく減らしたい、、

そうだ!CSS のセレクタを使えばいいんだ!

まず、このコードを見てください

* + * {
margin-top: 8px;
}

別名 owl selector (セレクタがフクロウの目に似てるから)。 あまり見ないセレクタですよね。

何をしているのか。説明していきましょう。

まず、* は universal selector ですね。全ての HTML 要素がターゲットです。

一見、これだけ見るとやばそうなセレクタですよね。笑

ですが、この後に来る + * が重要なのです。

+ は adjacent sibling selector と呼ばれていて、日本語では 隣接兄弟結合子っていうらしいですね。

これは、

2 つのセレクターを接続し、同じ親要素の子同士であって、1 つ目の要素の直後にある 2 つ目の要素を選択します

なんか難しいですね。例えば、以下のコードがあるとします。

<div className="stack">
<div>foo</div>
<div>fuga</div>
<div>hello</div>
</div>

そして、.stackdiv + div セレクタを付与すると、、

foo

fuga

hello

このように、スタイルが当てられたのは最初以外の要素となります。そして、子結合子(>)と同じように、直接の子要素の場合のみマッチします。

ですが、div + divの場合だと、2 つ目の要素がdivじゃないとマッチしません。なので、ここで出てくるのが * + *というわけですね。

* + *という書き方だと、

<div className="stack">
<span>foo</span>
<div>fuga</div>
<input {...inputProps} />
</div>

とあっても、全ての要素にマッチするので、

foo (span)

fuga (div)

ちゃんと一つ目以外の要素にスタイリングが当たっています。


ここで本題。これを駆使してレイアウトコンポーネントを作ってみましょう。

まぁ、と言っても先ほど説明したコードをコンポーネント化するだけ。

const Stack = ({ children }: { children: React.ReactNode }) => (
<div className="stack">{children}</div>
);

で CSS はこのように

.stack * + * {
margin-top: 4px;
}

簡単ですね。他のスペーシングを足したい場合は、props にspacingみたいな値を加えます。

これはデザインシステムなどを導入している場合、特に便利です。なぜかというと、決められた値しか受け付けないようにできるからです。

const Stack = ({
children,
spacing = "s",
}: {
children: React.ReactNode;
spacing: "s" | "m" | "l";
}) => <div className={`stack--${spacing}`}>{children}</div>;
/* global.css 簡易的なデザインシステムの例 */
:root {
--space-s: 4px;
--space-m: 8px;
--space-l: 16px;
}
.stack--s * + * {
margin-top: var(--space-s);
}
.stack--m * + * {
margin-top: var(--space-m);
}
.stack--l * + * {
margin-top: var(--space-l);
}

って感じで。TypeScript 使ってるとさらに autocomplete とかあるので、めちゃくちゃいい DX ですよね。

あとは適当に使うだけ。

import Stack from "../path/to/Stack";
<Stack spacing="m">
<div>Hello</div>
<input type="text" value="World" readOnly />
<div>Another div</div>
</Stack>;

↓   ↓   ↓  (border は間隔が分かりやすいように付けてます)

Hello

Another div

ちゃんと一つ目の要素以外にmargin-topが当たってますね。

これは縦スタックですが、簡単に横スタックにもできます。

.stack-vertical {
&__s * + * {
margin-top: var(--space-s);
}
&__m * + * {
margin-top: var(--space-s);
}
&__l * + * {
margin-top: var(--space-s);
}
}
/* margin-topをmargin-leftに変えるだけ */
.stack-horizontal {
display: flex;
flex-direction: "row";
&__s * + * {
margin-left: var(--space-s);
}
&__m * + * {
margin-left: var(--space-s);
}
&__l * + * {
margin-left: var(--space-s);
}
}

^のコードは試してないので、動くかは分かりません。イメージです。less で書いた方が分かりやすいかなと

そして、

const Stack = ({
children,
spacing = "s",
direction = "horizontal",
}: {
children: React.ReactNode;
spacing: "s" | "m" | "l";
direction: "horizontal" | "vertical";
}) => <div className={`stack-${direction}__${spacing}`}>{children}</div>;

を足してあげると、縦横方向の間隔開けに適したコンポーネントを作ることができます。

↓   ↓   ↓

Hello

Another div


どうでしょうか。フロントエンド構築に置いて考えることが一つ減りましたね。

長々となりましたが、最後まで読んでいただきましてありがとうございます。

© Yuya Oiwa