UIコンポーネントの大きさは外から制御しよう

昨今のフロントエンド向けUIライブラリでは、**コンポーネント**の設計が重要です。この記事では、コンポーネントのスタイリング、その中でもとくに**コンポーネントの大きさ**に関わるコンポーネント設計について考えます。

私の考える結論は、**むやみに大きさを指定できるpropを生やさずに、CSSで外から大きさを制御できるようにしたほうがいい**です。

## コンポーネントの大きさを制御したい

UIの一部分を再利用可能なコンポーネントとする場合、同じコンポーネントがさまざまな場面で使えるのが望ましいでしょう。コンポーネントが提供する機能にもよりますが、場面に応じてさまざまな大きさでコンポーネントを使用できたほうがよいこともあります。

具体例として、このようなコンポーネントを考えてみましょう。例はReactで示しますが、この記事の内容はReactとは関係ありません。

```tsx
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>
);
```

```css
.card {
  box-shadow: #999 4px 4px 4px 4px;
  border-radius: 10px;
  padding: 8px;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 16px;
}
```

この`Card`コンポーネントはこのように使います。

```jsx
<Card>Hello, world!</Card>
```

![Cardコンポーネントのレンダリング結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/26e4c632-9997-996a-3406-79e5f675efda.png)

このように、アイコンと中身のコンテンツが枠で囲まれたUIを描画するのが`Card`の役割ですね。

### 横幅を制御する

画像を見ると、カードが横に長いように思います。適切な横幅にするためにカードの横幅を指定したい場合はどうすればよいでしょうか。

:::note warn
最近のCSSのトレンドは論理プロパティ名なので「横幅」と言わずに「インラインの大きさ」と言うのが適切なのですが、この記事では分かりやすさを優先して縦・横と言います。
:::

あまりおすすめではない方法は、`Card`にwidth propを追加することです。

```tsx
// おすすめではない方法
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>
```

![幅が250pxになったカードの画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/3a54bf18-745c-96f5-b3ad-fd7090d35d16.png)

これがおすすめではない理由は、柔軟性に乏しいからです。上の例でもすでにその片鱗が出ており、`width`を`number`に制限しています。これはのちのち悩みの種となります。

この場合の望ましい方法は、**親要素の横幅で制御する**ことです。つまり、このようにします。

```tsx
<div style={{ width: "250px" }}>
  <Card>Hello, world!</Card>
</div>
```

`Card`の一番外側のdiv要素は`display: flex;`であり、この場合は利用可能な横幅いっぱいに広がるのがデフォルトの挙動です。親要素を250pxに制限してしまうことで、`Card`のdiv要素は250pxまでしか広がらなくなります。

:::note
`width: 250px` を指定したらそのボックスは横幅いっぱいに広がっていないように見えますが、実は左右のマージンも合わせると横幅いっぱいに広がります。`margin-inline: auto;`でボックスを中央に配置できるのはそのためです。
:::

このようにすることの利点は、CSSの表現力が制限されないことです。例えば、カードの大きさが中身の大きさに従ってほしいと思った場合は、`width: max-content;`です。

```tsx
<div style={{ width: "max-content" }}>
  <Card>Hello, world!</Card>
</div>
```

![親にwidth: max-content;を指定したカードの画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/128d16ae-2d2c-4e9a-1a44-bc50f7166776.png)

もし`width: max-content;`が必要になったとき、`width: number;`をpropsで受け取る設計だと破綻してしまいます。`width: number | "max-content";`みたいにする必要があるかもしれません。

このように、propsで`width`を制御しようなどとは思わずに、最初からCSSで任せるのが最も柔軟かつ機能的な状態であり、望ましいと思います。

### 縦の大きさの制御

ところで、コンポーネントの縦の大きさを制御したい場合はどうすればいいでしょうか。CSSにおいて、特に`height`を指定していないブロックの大きさは、その中身が入る最低限の大きさになります。勝手に広がってくれるものではないため、横幅とは勝手が違います。

やりがちなのは、`height: 100%;`を指定できる `fullHeight` みたいなpropを作る方法です。

```tsx
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>
```

![縦幅が120pxになったカードの画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233667/cf8d2b01-96aa-2fd4-eb2f-6b9bb69c407b.png)

昔はこのようなpropが必要になるのも仕方がなかったように思います。しかし、今のCSSの表現力であれば、`fullHeight` propも不要になります。

こうすればいいのです。

```tsx
<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`コンポーネントの仕様の一部なのです。仕様はコンポーネントのユーザーへの約束事、あるいは規約であると理解することもできます。

:::note
上では簡単に`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を作る必要がなくなります。逆に、コンポーネントがどんな使われ方をしても（親コンポーネントがボックスを外から伸ばしてきたとしても）耐えて適切に描画されるように実装する必要があります。

:grinning: