gaaamiiのブログ

間違ったことを書いている時があります。コメントやTwitter、ブコメなどでご指摘ください

Flexboxコンテナの左右の余白をいい感じにする

Flexboxコンテナの左右の余白をいい感じにしたい!でもできない!と悩んだので、それを共有します。最終的に、CSSだけで書くのは諦めてJavaScriptを書きました。悔しい!!

なにを実現したいのか

いくつかのアイテムを持つFlexboxコンテナで、左揃えで、画面内におさまるように折り返して、コンテナの左右の余白は均等になっているやつをCSSで書きたかった。

こういうやつです。

やってみよう

こんなん楽勝でしょ。と手を付けたんですが、楽勝ではありませんでした。

折り返す

まずは折返しです。これはもう、flex-wrap: wrap; を指定すればいいだけです。これはすぐできました。

/* 値を設定するだけなら変数にする必要はないのだけど、*/
/* このときはcalc()などを活用してCSSだけでいけると思っていた... */
:root {
  --cardWidth: 240px;
  --cardHeight: 280px;
  --cardMargin: 16px;
}
.container {
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
  border: 1px solid #ccc;
}
.card {
  width: var(--cardWidth);
  height: var(--cardHeight);
  margin: var(--cardMargin);
  border: 1px solid #ccc;
  border-radius: 8px;
  background: #fff;
  box-sizing: border-box;
}

See the Pen Flexboxでレスポンシブなやつ試作その1 by gaaamii (@gaaamii-the-sasster) on CodePen.

折返したのはいいのですが、コンテナの左右の余白がいまいちですね。

左右の余白を均等にする

コンテナの全体の余白を、どう指定するべきか。コンテナに対して margin: 0 auto; で済めば嬉しいのですが、そうはいきませんでした。インスペクタで幅を見てみると、コンテナの幅は、その親要素いっぱいの幅になっていました。

どうしよう。そもそもコンテナの幅があってその中で幅を分配するというのが自然ですが、今回の場合、アイテムとそのパディングが先に決められていて、そこからコンテナの幅が決まってほしいわけです。

今回の場合、コンテナの幅を決めるためには、「一行にいくつのアイテムが並べられているか」がわかればよさそうだと考えました。

アイテムが1行にn個表示されているとしたら、コンテナの幅は、

コンテナの幅 = (アイテムの幅 * n) / 2

です。では、nはどうやって出せば良いのでしょう。

n = コンテナの親要素の幅 / アイテムの幅

です。これくらいの算数なら自分でもできるぞ!さっそく実装してみます。

JavaScriptの力を借りる

CSSは先程と同じです。で、それに加えてこんなJavaScriptを書きました。

const CARD_PADDING = 32 // カード1つにつける余白

const state = {
  container: null, // コンテナ要素への参照
  card: null, // カード要素への参照
  numberOfItems: null, // カードが1行にいくつ表示されているかの状態
}

const initState = () => {
  state.container = document.getElementsByClassName('container')[0]
  state.card = document.querySelector('.card')
}

// 現在のウィンドウ幅にいくつカードが表示できるかを計算
// カードの表示可能な数が変わったら, それをもとにコンテナ要素の幅を更新する
const updateContainerWidth = () => {  
  const cardAllWidth = state.card.offsetWidth + CARD_PADDING
  const numberOfItems = Math.floor(window.innerWidth / cardAllWidth)
  if (state.numberOfItems !== numberOfItems) {
    state.numberOfItems = numberOfItems
    const containerWidth = state.numberOfItems * cardAllWidth
    state.container.setAttribute("style", `width: ${containerWidth}px`)
  }
}

window.onload = () => {
  initState()
  updateContainerWidth()
}

window.onresize = () => {
  updateContainerWidth()
}

できました。実際のアプリケーション開発に取り入れるには、Reactなりなんなりの書き方になおさないといけないですが、できることがわかったのでよしです。

See the Pen Flexboxでレスポンシブなやつ試作その2 by gaaamii (@gaaamii-the-sasster) on CodePen.

なぜCSSだけでできなかったのか

CSSだけでいけそうじゃん!という感じですが、なぜ今回CSSだけでいけなかったのか。書き始めたときは、以下のCSSで実現できると思っていました。上記のJavaScriptでやっていることと同じことを、最初はCSSでやろうとしていたのです。

:root {
  --cardWidth: 240px;
  --cardHeight: 280px;
  --cardMargin: 16px;
  --cardAllWidth: calc(var(--cardWidth) + (var(--cardMargin) * 2));
}
.container {
  --numberOfItems: calc(100vw / var(--cardAllWidth));
  --containerWidth: calc(var(--cardAllWidth) * var(--numberOfItems));
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
  border: 1px solid #ccc;
  width: var(--containerWidth);
}
.card {
  width: var(--cardWidth);
  height: var(--cardHeight);
  margin: var(--cardMargin);
  border: 1px solid #ccc;
  border-radius: 8px;
  background: #fff;
  box-sizing: border-box;
}

ですが、このCSSには問題があります。

CSSのcalc() では、非number値による除算はできません。

/

右側は <number> でなければなりません。

https://developer.mozilla.org/ja/docs/Web/CSS/calc

なので、実際にはこの行でほしい値は出ませんでした(var(--cardAllWidth)はnumberではなくlength)。

--numberOfItems: calc(100vw / var(--cardAllWidth));

雑感

CSSの変数とcalc()があればなんでもできるだろ!という気持ちで書き始めたのですが、見事に敗北しました。CSS難しい...。もし、CSSだけでも書けるじゃんと思った方いたら、教えていただけると嬉しいです。