283
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

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

Last updated at Posted at 2024-07-02

昨今のフロントエンド向け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>

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>

幅が250pxになったカードの画像

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

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

<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: 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>

縦幅が120pxになったカードの画像

昔はこのような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要素だとしても、その時点で「一番外側のボックスはブロックレベルボックスである」という暗黙の仕様が発生しているのです。

逆に、これを明示的に意識して活かす設計にすることで、widthheightといった propをわざわざ作らないとか、そういう意思決定ができるようになるでしょう。

UIコンポーネントはデザインを元に実装することも多くあるでしょう。その場合、コンポーネントの大きさに関する仕様は明確にしておくべきでしょう。基本的には2択で「完全に大きさ固定」か「親からの影響に従う」です。CSSがどのように要素のレイアウトを行うのか理解し、CSSの荒波を生き抜くコンポーネントを作りましょう。

まとめ

この記事で紹介したように、CSSでは子要素の大きさを親要素から制御できます。親要素の領域いっぱいまで伸ばすことも可能なのです。

UIコンポーネントを実装するときは、そのコンポーネントのボックスがCSSにおいてどのような振る舞いを持つボックスなのかを意識し、コンポーネントの仕様の一部として認識すべきです。

そして、コンポーネントのボックスがブロックレベルボックスなのであれば、コンポーネントのボックスが親から伸ばされることを前提に設計するのがよいと思います。

そうすれば、widthheightといったpropsを作る必要がなくなります。逆に、コンポーネントがどんな使われ方をしても(親コンポーネントがボックスを外から伸ばしてきたとしても)耐えて適切に描画されるように実装する必要があります。

:grinning:

283
144
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
283
144

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?