CSS Flexboxのレイアウトで起きる厄介な問題をJavaScriptを使用せずに、解決するテクニック
Post on:2019年1月31日
CSS Flexboxの利点は柔軟なレイアウトを行えることです。しかし、そのフレキシブルさがネックになってしまうことがあります。
ここ数ヵ月の間、あちこちのブログやコミュニティで議論されていたFlexboxのレイアウトで起きる厄介な問題が解決したようなので、紹介します。
The Flexbox Holy Albatross
by Heydon Pickering
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
はじめに
私はこの6ヵ月の間、さまざまな活動に取り組んでいました。
※これらに限定されるものではありません。
- 頭をかきむしる
- 窓の外をみつめる
- ベッドで天井をみつめる
- 枕に叫ぶ
- 呪文をつぶやきながら、床を転げ回る
と同時に、CSSレイアウトの厄介な問題に取り組んでいました。
そして、今朝の午前5時にこの解説方法が私の頭に舞い降りたのです。
この解説方法をあなたと共有したいと思います。
CSSレイアウトの厄介な問題
Flexboxでは、ラッピング(flex-wrap: wrap)とグロウイング(flex-grow: 1)を組み合わせて、すべてのflexアイテムをビューポートのさまざまな幅に正しい位置に配置できます。手間のかからないレスポンシブなレイアウトをFlexboxは簡単に実現できます。
厄介な問題とは、アイテムを特殊なルールで配置したい場合です。
例えば、アイテムが3つある場合、3つが横並びの1行に配置されます。しかし、幅が狭くなった際に、1行に2つの要素とその下に長い1つの要素で構成されることは避けたいと考えるかもしれません。
左:3つが横並び、中:2つの要素とその下に長い1つの要素、右:期待する配置
どうしてこうなるのでしょうか?
この3つは同等のアイテムなので、1つが異なって表示されてしまうと、その3つは同等には見えません。これでは3つ目の長いアイテムが特別になり、ユーザーはそのように認知します。3つ目が他の2つより重要なアイテムですか?
コンテナの幅が狭い際に上図の中の配置を経由することなく、左の横並びから、右の積み重ねにすぐに切り替えるにはどうすればよいでしょうか?
Media Queryで実装する -ばかげてます
これは望む解決方法ではありません。実際に、私はこの方法を絶対拒否します。クリーンなコードを書くことに私が自意識過剰であるという理由だけではありません。@mediaのブレークポイントがシステムを設計するために受け入れ難いものだからです。
コンテキスト外で定義された私のコンポーネントは、任意の幅でコンテナや親要素内にインスタンス化できます。コンテキストは変化します。しかし、@mediaはビューポートである定数に従って調整しなければなりません。
メディアクエリだと、コンテンツが狭い場合には対応できない
実際に、この方法は使い物になりません。コンテナクエリ(ビューポートの代わりに直接コンテナ要素に対応するブレークポイント)のために非常に多くの人々がコードを書き、戦って、共感してきた理由です。
参考: Container Query Disbussion
問題は、コンテナクエリがブラウザから多くのものを要求し、誰もが納得するパフォーマンスの高いネイティブのCSSでの解決方法を思い付かないことです。
JavaScriptを使用するとどうなるでしょう、、、
訳者注:
幅に応じてレイアウトを変更する際、スクリーンの幅を使用してメディアクエリを定義する場合が多いと思います。しかし、最近のWebサイトやアプリではコンポーネントベースで設計されることが増え、この方法だけでは限界がでてきます。そこで登場したのが、コンテナクエリです。コンテナクエリはスクリーンの幅を基準にするのではなく、指定したセレクタの幅を基準にその子要素のセレクタに対してCSSを適応する方法です。
コンポーネントがどの種類の親要素になっているのか分からないコンポーネントを作成している場合、素晴らしい解決策となります。
JavaScriptで実装する
ResizeObserverを使用すると、非常に少ないコードで要素とコンテナクエリを生成できます。私は今朝早くに、このコードを思いつきました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
export default function markBreak(elem, width) { if (ResizeObserver) { const test = document.createElement('div'); test.classList.add('test'); test.style.width = width; elem.appendChild(test); let br = test.offsetWidth; elem.removeChild(test); const ro = new ResizeObserver( entries => { for (let entry of entries) { const cr = entry.contentRect; const q = cr.width <= br; entry.target.classList.toggle('lt-' + width, q); entry.target.classList.toggle('gte-' + width, q); } }); ro.observe(elem); } } |
ResizeObserverについては素晴らしい解説記事があります。
参考: ResizeObserver: It’s Like document.onresize for Elements
3つのアイテムが40remから積み重なるようにするには、markBreak(container, '40rem')とし、CSSを下記のように書くだけです。
1 2 3 4 5 6 7 |
.container > * { width: 100%; } .container.gte-40rem > * { width: 33%; } |
悪くない理由
- ResizeObserverは効率のために最適化されています。onresizeを使用するよりもはるかに面倒です。
- これはプログレッシブエンハンスメントです。JavaScriptが実行されない場合、またはResizeObserverがサポートされていない場合、コンテナの幅に関係なく、ユーザーは単一のカラムを取得します。
- test要素を使用すると、任意の単位を使用してブレークポイントを選択できます。相対的なサイズは現在のコンテキストのピクセルに変換されるため、20emは正確です(20remはページ内の別の場所にある何か他のものを意味する可能性があります)。
しかし、実際にはかなり悪いです
- JavaScriptは実行されない可能性があります。また、実行されると常に集中的に負荷がかかります。それはあなたが肛門から大量に出血するような感じです。
- ResizeObserverは現在、Chromeでのみサポートされているだけです。
では、CSSのみで解決する方法はあるのでしょうか?
はい、あります。厳密に言うとHTMLとCSSで解決できます。CSSのカスタムプロパティとcalc()関数を使用します。
CSSで解決
結論を急ぐ前に、ますは落ち着いて考えてみましょう。
私は、flex-basisを使った素晴らしい解決方法を見つけました。つまり、任意の数の水平要素を垂直に配置を切り替えることができます。The Flexbox Holy Albatross Reincarnatedを見てみてください。
そこにあるコードを解説します。
コンポーネントはフレキシブルで、伸びたり縮んだりします。そして、アイテムは一行に3つか1つなので、そのコンテナの幅の33%か100%を占めることを望みます。これを簡単に実現するには、要素が33%、または100%を超えないように定義します。
1 2 3 4 5 |
.container > * { min-width: 33%; max-width: 100%; flex-grow: 1; } |
flex-growを使用しているのは、33.33333333333%と完全に書くことに煩わされないためです。それぞれのカラムが正確にスペースの1/3を占めることができます。
flex-basisプロパティは任意のアイテムの基本的となる幅(初期値)を設定します。アイテムはflex-basisの値で伸び縮みします。厳密には、min-widthとmax-widthの値がflex-basisを上書きします。従って、flex-basisの値が999remのように極端に高い場合は、幅は100%になります。また、-999remのように極端に低い場合は、33%(または33.333333333333%、flex-growに感謝)になります。
参考: CSS Flexboxの各プロパティの使い方を詳しく解説
値が高い場合は100%に、低い場合は33%に
重要なのは、このスイッチを適切なポイントに作ることです。議論をしやすくするために、このポイントを40remとしましょう。40rem未満ではアイテムは一列に配置され、40rem以上ではそれぞれが行に配置されます。
コンテナに、カスタムプロパティ--multiplierを定義します。
1 2 3 4 5 |
.container { display: flex; flex-wrap: wrap; --multiplier: calc(40rem - 100%); } |
これで、コンテナが40rem以上の場合は値は正になり、40remより狭い場合は負になります。flexアイテムではこれを乗数として使用して、非常に高いまたは非常に低いflex-basisの値を設定します。999にしたのは、ただの慣例です。
1 2 3 4 5 6 |
.container > * { min-width: 33%; max-width: 100%; flex-grow: 1; flex-basis: calc(var(--multiplier) * 999); } |
広い場合は行に、狭い場合は列に配置、そしてコンテンツが狭い場合も対応
追加情報:
Jonathan Snookが記事で指摘するように、カスタムプロパティは特に必要ではありません。私は子要素の計算の一部として値を含めることができると説明するべきでした。なぜなら、100%は親と子で同じだからです。しかし私は読みやすくするために、名前を付けて設定することを好みます。動的なカスタムプロパティの値が自動的に継承される方法も優れています。そうすることで、もっと面白いことができるかもしれません。
マージンが必要ないなら、ネガティブマージンのテクニックを使うことができますが、切り替わることを確実にするためにmin-widthからマージンを取り除く必要があります。
参考: Making Future Interfaces: Algorithmic Layouts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.container { display: flex; flex-wrap: wrap; --margin: 1rem; --multiplier: calc(40rem - 100%); margin: calc(var(--margin) * -1); /* excess margin removed */ } .container > * { min-width: calc(33% - (var(--margin) * 2)); /* remove from both sides */ max-width: 100%; flex-grow: 1; flex-basis: calc(var(--multiplier) * 999); margin: var(--margin); } |
gapプロパティがブラウザに広くサポートされるようになれば、これは冗長になります。GridやFlexbox、そしてその他のコンテキストのためでなく、grid-gapの存在も覚えておいてください。
このコードは、さまざまな数の要素やさまざまな幅の要素を処理することもできます。例えば、60remに4つの要素で奇数の要素を20%、偶数の要素を30%にしたいとします。Flexbox、カスタムプロパティ、calc()関数、nth-childが非常によく機能します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
.container { display: flex; flex-wrap: wrap; --margin: 1rem; --multiplier: calc(60rem - 100%); margin: calc(var(--margin) * -1); } .container > * { max-width: 100%; flex-grow: 1; flex-basis: calc(var(--multiplier) * 999); margin: var(--margin); } .container > :nth-child(2n - 1) { min-width: calc(20% - (var(--margin) * 2)); } .container > :nth-child(2n) { min-width: calc(30% - (var(--margin) * 2)); |
このコードで「*」は使用しているため、他で重複しないように注意してください。
CodePenにこのデモをアップロードしました。
See the Pen
Flexbox One Column Switch by Heydon (@heydon)
on CodePen.
こういったアルゴリズムのレイアウトに興味があるなら、下記の動画もお勧めします。
sponsors