昨今のフロントエンド向けUIライブラリでは、コンポーネントの設計が重要です。この記事では、コンポーネントのスタイリング、その中でもとくにコンポーネントの大きさに関わるコンポーネント設計について考えます。
私の考える結論は、むやみに大きさを指定できるpropを生やさずに、CSSで外から大きさを制御できるようにしたほうがいいです。
コンポーネントの大きさを制御したい
UIの一部分を再利用可能なコンポーネントとする場合、同じコンポーネントがさまざまな場面で使えるのが望ましいでしょう。コンポーネントが提供する機能にもよりますが、場面に応じてさまざまな大きさでコンポーネントを使用できたほうがよいこともあります。
具体例として、このようなコンポーネントを考えてみましょう。例はReactで示しますが、この記事の内容はReactとは関係ありません。
const Card: React.FC<React.PropsWithChildren> = ({ children }) => (
<div className="card">
<img
src="https://jdecked.github.io/twemoji/v/latest/svg/1f600.svg"
width="64"
height="64"
alt=""
/>
<div>{children}</div>
</div>
);
.card {
box-shadow: #999 4px 4px 4px 4px;
border-radius: 10px;
padding: 8px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
}
このCard
コンポーネントはこのように使います。
<Card>Hello, world!</Card>
このように、アイコンと中身のコンテンツが枠で囲まれたUIを描画するのがCard
の役割ですね。
横幅を制御する
画像を見ると、カードが横に長いように思います。適切な横幅にするためにカードの横幅を指定したい場合はどうすればよいでしょうか。
最近のCSSのトレンドは論理プロパティ名なので「横幅」と言わずに「インラインの大きさ」と言うのが適切なのですが、この記事では分かりやすさを優先して縦・横と言います。
あまりおすすめではない方法は、Card
にwidth propを追加することです。
// おすすめではない方法
const Card: React.FC<
React.PropsWithChildren<{
width?: number;
}>
> = ({ children, width }) => (
<div
className="card"
style={{ width: width !== undefined ? `${width}px` : undefined }}
>
<img
src="https://jdecked.github.io/twemoji/v/latest/svg/1f600.svg"
width="64"
height="64"
alt=""
/>
<div>{children}</div>
</div>
);
// 使い方
<Card width={250}>Hello, world!</Card>
これがおすすめではない理由は、柔軟性に乏しいからです。上の例でもすでにその片鱗が出ており、width
をnumber
に制限しています。これはのちのち悩みの種となります。
この場合の望ましい方法は、親要素の横幅で制御することです。つまり、このようにします。
<div style={{ width: "250px" }}>
<Card>Hello, world!</Card>
</div>
Card
の一番外側のdiv要素はdisplay: flex;
であり、この場合は利用可能な横幅いっぱいに広がるのがデフォルトの挙動です。親要素を250pxに制限してしまうことで、Card
のdiv要素は250pxまでしか広がらなくなります。
width: 250px
を指定したらそのボックスは横幅いっぱいに広がっていないように見えますが、実は左右のマージンも合わせると横幅いっぱいに広がります。margin-inline: auto;
でボックスを中央に配置できるのはそのためです。
このようにすることの利点は、CSSの表現力が制限されないことです。例えば、カードの大きさが中身の大きさに従ってほしいと思った場合は、width: max-content;
です。
<div style={{ width: "max-content" }}>
<Card>Hello, world!</Card>
</div>
もしwidth: max-content;
が必要になったとき、width: number;
をpropsで受け取る設計だと破綻してしまいます。width: number | "max-content";
みたいにする必要があるかもしれません。
このように、propsでwidth
を制御しようなどとは思わずに、最初からCSSで任せるのが最も柔軟かつ機能的な状態であり、望ましいと思います。
縦の大きさの制御
ところで、コンポーネントの縦の大きさを制御したい場合はどうすればいいでしょうか。CSSにおいて、特にheight
を指定していないブロックの大きさは、その中身が入る最低限の大きさになります。勝手に広がってくれるものではないため、横幅とは勝手が違います。
やりがちなのは、height: 100%;
を指定できる fullHeight
みたいなpropを作る方法です。
const Card: React.FC<
React.PropsWithChildren<{
fullHeight?: boolean;
}>
> = ({ children, fullHeight }) => (
<div className="card" style={{ height: fullHeight ? "100%" : "auto" }}>
<img
src="https://jdecked.github.io/twemoji/v/latest/svg/1f600.svg"
width="64"
height="64"
alt=""
/>
<div>{children}</div>
</div>
);
// 使い方
<div style={{ width: "max-content", height: "120px" }}>
<Card fullHeight>Hello, world!</Card>
</div>
昔はこのようなpropが必要になるのも仕方がなかったように思います。しかし、今のCSSの表現力であれば、fullHeight
propも不要になります。
こうすればいいのです。
<div style={{ width: "max-content", height: "120px", display: "grid" }}>
<Card>Hello, world!</Card>
</div>
そう、親がgridレイアウトであれば何も指定されていない子要素の高さを伸ばせるのです。
今どきのCSSでは、ただのボックスの大きさというのは親の側から自由自在に操作できるものです。
Gridレイアウトが普及していなかった時代はflexレイアウトに頼ることになるのですが、その場合はクロス軸方向にはalign-items: stretch
で伸ばすことができました。メイン軸方向はそのままでは伸びないので、コンポーネントがflex={true}
というpropを取れるようにしていたことがあります。flex={true}
のときはコンポーネントのボックスにflex: 1;
が与えられてメイン軸方向にも伸びるようになるという機能です。
コンポーネントの仕様を意識する
UIコンポーネントは、仕様が定まっている必要があります。ここでいう仕様とは、それが変わると破壊的変更になるコンポーネントの挙動のことです。例えば、Card
の一番外のdivがdisplay: flex;
から display: inline;
に変わったら上で説明したことは成り立たなくなりますから、Card
の一番外のdivがdisplay: flex;
であることはCard
コンポーネントの仕様の一部なのです。仕様はコンポーネントのユーザーへの約束事、あるいは規約であると理解することもできます。
上では簡単にdisplay: flex;
であることを仕様と表現しましたが、細かく言えば少し異なります。
正確な説明としては、そのコンポーネント全体のボックスがCSSのレイアウトの挙動においてどのように振る舞うのかがコンポーネントの規約であり、それを変えるのが破壊的変更になります。
そのため、例えばdisplay: flex;
をdisplay: block;
とかdisplay: grid;
に変えるのは破壊的変更にあたりません。
display
プロパティの2値記法ではdisplay: flex;
はdisplay: block flex;
であり、block
は外から見たらボックスがブロックレベルボックスであることを意味し、flex
はボックスの中身がflexレイアウトになるという意味です。
display: block;
とdisplay: grid;
はそれぞれdisplay: block flow;
とdisplay: block grid;
であり、外から見たらブロックレベルボックスであることは変わっていないのです。
このように、CSSの世界に生きるUIコンポーネントを作る際には、そのコンポーネントが作るボックスがCSSにおいてどのように振る舞うのかということも、(時に暗黙のうちに)コンポーネントの仕様の一部となります。たとえ一番外側が何の変哲もないdiv要素だとしても、その時点で「一番外側のボックスはブロックレベルボックスである」という暗黙の仕様が発生しているのです。
逆に、これを明示的に意識して活かす設計にすることで、width
や height
といった propをわざわざ作らないとか、そういう意思決定ができるようになるでしょう。
UIコンポーネントはデザインを元に実装することも多くあるでしょう。その場合、コンポーネントの大きさに関する仕様は明確にしておくべきでしょう。基本的には2択で「完全に大きさ固定」か「親からの影響に従う」です。CSSがどのように要素のレイアウトを行うのか理解し、CSSの荒波を生き抜くコンポーネントを作りましょう。
まとめ
この記事で紹介したように、CSSでは子要素の大きさを親要素から制御できます。親要素の領域いっぱいまで伸ばすことも可能なのです。
UIコンポーネントを実装するときは、そのコンポーネントのボックスがCSSにおいてどのような振る舞いを持つボックスなのかを意識し、コンポーネントの仕様の一部として認識すべきです。
そして、コンポーネントのボックスがブロックレベルボックスなのであれば、コンポーネントのボックスが親から伸ばされることを前提に設計するのがよいと思います。
そうすれば、width
やheight
といったpropsを作る必要がなくなります。逆に、コンポーネントがどんな使われ方をしても(親コンポーネントがボックスを外から伸ばしてきたとしても)耐えて適切に描画されるように実装する必要があります。