Web制作者はしっかりと理解しておきたい! CSSにおけるレイアウトアルゴリズムを解説
Post on:2022年4月14日
CSSを理解するには、プロパティや値を知っているだけでは不十分です。CSSのレイアウトアルゴリズムがどのように機能するかを理解する必要があります。
ブログやツイートで便利なCSSスニペットが紹介されていても、なぜ機能するのか、レイアウトアルゴリズムがどのように使用されているのか説明されていないことはよくあります。CSSにおけるレイアウトアルゴリズムについて解説します。
CSSの初学者をはじめ、長く携わっている人にも、気づきや学びがあると思います。
Understanding Layout Algorithms
by Josh W. Comeau
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
はじめに
数年前、私はCSSで「エウレカっ!」な瞬間を体験しました。
※エウレカとは、アルキメデスが発見した時に叫んだ言葉
それまで私は、CSSの学習はz-index: 10;
やjustify-content: center;
のようなプロパティと値に着目していました。それぞれのプロパティが何をするのかを理解すれば、CSS全体を深く理解できると考えたのです。
そこで気づいたのは、CSSは単なるプロパティの集合体ではないということです。CSSは相互に接続されたレイアウトアルゴリズムの集合体なのです。それぞれのアルゴリズムは、独自のルールとメカニズムを持つ複雑なシステムです。
特定のプロパティが何をするのかを学ぶだけでは十分ではありません。レイアウトアルゴリズムがどのように機能するか、プロパティをどのように使用するかを学ぶ必要があります。
これまで何度も使用した馴染みのあるCSSを書いたのに、期待とは違う結果になり、不安になったことはありませんか? それはとてもイライラします。言語としての一貫性がなく、不安定に感じてしまうかもしれません。まったく同じCSSを入力して、どうして違う出力になるのでしょうか?
これは、プロパティが複雑なシステムに作用しており、プロパティの動作を変更する微妙なコンテキストがあるために発生します。わたし達のメンタルモデルは不完全であり、それが驚きにつながります!
レイアウトアルゴリズムを掘り下げると、すべてがより理解できるようになりました。何年も悩まされてきた謎が解けました。CSSは実に堅牢な言語であることがわかり、書くのが楽しくなりました。
この記事ではCSSで何が起きているかを理解するために、この新しいレンズがどのように役立つかを解説したいと思います。そしてそのレンズを使って、意外と知られていない謎を解き明かします 🕵️
CSSにおけるレイアウトのアルゴリズム
CSSにおける「レイアウトアルゴリズム」とは何でしょうか?
すでにご存知の方もいると思いますが、その一部をご紹介します。
- Flexbox
- Positioned (position: absolute; など)
- Grid
- Table
- Flow
正確には、これらはレイアウトアルゴリズムではなく、「レイアウトモード」と呼ばれます。しかし、「レイアウトアルゴリズム」の方がより適切なラベルだと思います。
ブラウザがHTMLをレンダリングすると、すべての要素のレイアウトはプライマリ レイアウトアルゴリズムを使用して計算されます。そして特定のCSS宣言を使用することで、異なるレイアウトアルゴリズムを選択することができます。たとえば、position: absolute;
を適用すると、Positionedレイアウトを使用するように要素が切り替わります。
例を見てみましょう。次のようなCSSがあるとします。
1 2 3 |
.box { z-index: 10; } |
最初の課題は、.box
要素のレンダリングにどのレイアウトアルゴリズムが使用されるかを理解することです。上記のCSSでは提供されたプロパティに基づいて、Flowレイアウトを使用してレンダリングされます。
Flowは、WebのOGレイアウトアルゴリズムです。これはWebが世界最大のアーカイブのようなハイパーリンクで結ばれた巨大なドキュメント群として捉えられていた時代に作られたものです。Microsoft Wordなどのワープロソフトで使用されているレイアウトアルゴリズムに似ています。
Flowは、table
以外のHTML要素に使用されるデフォルトのレイアウトアルゴリズムです。他のレイアウトアルゴリズムが明示的に指定されない限り、Flowが使用されます。
z-index
プロパティは重なり順を制御するために使用されるもので、重なったときにどちらが上に表示されるかを設定します。ただし、ここで問題なのは、Flowレイアウトには実装されていないことです。Flowはドキュメント形式のレイアウトを作成するためのものであり、要素が重なることを許容するワープロソフトは見たことがありません。
もしあなたが数年前に私に尋ねていたら、次のように答えたでしょう。
z-index
プロパティはposition
プロパティに依存するため、position
をrelative
やabsolute
などに設定しないとz-index
を使用することはできません。
これは正確には間違いではないのですが、小さな誤解があります。z-index
プロパティはFlowレイアウトアルゴリズムに実装されていないので、このプロパティを有効にするには別のレイアウトアルゴリズムにする必要があると言った方が正確です。
これはわざと難しい言い回しをしているように見えるかもしれませんが、この小さな誤解が大きな混乱につながることがあります。
たとえば、次の例をご覧ください。
このデモでは、Flexboxレイアウトアルゴリズムを使用して 3つの兄弟が配置されています。真ん中のピンクの要素にz-index
が設定されており手前に配置されています。z-index
を削除すると、他の兄弟より奥に配置されます。
どうしてそうなるのでしょうか?
どこにもposition: relative;
は設定していません。
答えは、Flexboxアルゴリズムがz-index
プロパティを実装しているために機能します。言語作成者がFlexboxアルゴリズムを設計したときにPositionedレイアウトで行うのと同様に、z-index
プロパティを接続して重ね順を制御することにしたからです。
これは重要なメンタルモデルの変化です。CSSのプロパティには、それ自体には意味がないということです。プロパティが何をするのか、どのように計算に使用されるのかは、レイアウトアルゴリズム次第です。
はっきり言って、どのレイアウトアルゴリズムでも同じように機能するCSSプロパティはいくつかあります。たとえば、color: red;
はどのアルゴリズムでもレッドのテキストを生成します。ただし、各レイアウトアルゴリズムは、どのプロパティについてもデフォルトの動作を上書きできます。また、多くのプロパティはデフォルトの動作がありません。
ここで、私を驚かせた例を紹介します。
width
プロパティがレイアウトアルゴリズムに応じて異なる方法で実装されていることをご存知ですか?
下記がその証拠です。
.item
要素には、width: 2000px;
というCSSのプロパティがあります。
.item
の最初のインスタンスはFlowレイアウトを使用してレンダリングされ、実際には2000pxの幅を消費します。Flowレイアウトでは、width
は厳格なハードルールです。結果はともかく、2000pxのスペースが消費されることになります。
.item
の2番目のインスタンスはFlexコンテナ内にレンダリングされます。つまり、Flexboxレイアウトが使用されていることになります。Flexboxアルゴリズムでは、width
はどちらかというとプロポーザルで、必ずそうなるとは限りません。
Flexboxの仕様では、これを「仮想サイズ(hypothetical size)」と呼んでいます。要素になんらかの制約や力が作用しない世界において、要素がそうなるであろうサイズです。理想的な世界では、この要素の幅は2000pxになりますが、幅の狭いコンテナに配置されているため、それに合わせて縮小されます。
ここでもう一度、フレームが非常に重要です。Flexboxの場合、width
に特別な注意が必要なわけではありません。Flexboxアルゴリズムは、Flowアルゴリズムとは異なる方法でwidth
プロパティを実装しているということです。
わたし達が書いたCSSのプロパティは、関数に渡される引数のような単なる入力です。つまり、入力をどう処理するのかは、レイアウトアルゴリズム次第になります。CSSを理解するには、レイアウトアルゴリズムがどのように機能するかを理解する必要があります。プロパティを知るだけでは不十分なのです。
レイアウトアルゴリズムの確認
CSSにlayout-mode
というプロパティはありません。使用されるレイアウトアルゴリズムを微調整できるプロパティがいくつかあり、実際にはかなり厄介です。
場合によっては、要素に適用されたCSSプロパティが特定のレイアウトモードになることがあります。たとえば、次のような場合です。
1 2 3 4 5 6 7 8 9 10 11 |
.help-widget { /* この宣言により、Positionedレイアウトを使用します。 */ position: fixed; right: 0; bottom: 0; } .floated { /* この宣言により、Floatレイアウトを使用します。 */ float: left; margin-right: 32px; } |
それ以外の場合は、親がどのようなCSSを適用しているかを調べる必要があります。たとえば、次のような場合です。
1 2 3 4 5 6 7 8 9 10 |
<style> .row { display: flex; } </style> <ul class="row"> <li class="item"></li> <li class="item"></li> <li class="item"></li> </ul> |
display: flex;
を適用する場合、実際には.row
要素にFlexboxレイアウトアルゴリズムを使用するのではなく、その子要素をFlexboxレイアウトで配置するように指示します。
技術用語では、display: flex;
はFlexフォーマットのコンテキストを作成します。すべての直接の子はこのコンテキストに加わります。つまり、デフォルトのFlowレイアウトではなく、Flexboxレイアウトを使用することを意味します。
display: flex;
は<span>
などのインライン要素をブロックレベルの要素に変換するため、親要素のレイアウトに何らかの影響をあたえます。ただし、使用されるレイアウトアルゴリズムは変更されません。
レイアウトアルゴリズムのバリエーション
レイアウトアルゴリズムの中には、複数のバリエーションに分かれているものがあります。
たとえば、Positionedレイアウトを使用する場合、いくつかの異なる「位置決め方式(positioning schemes)」を参照します。
relative
absolute
fixed
sticky
各バリエーションは独自のミニレイアウトアルゴリズムのようなものですが、共通点はあります(たとえば、すべてz-index
プロパティを使用できます)。
同様に、Flowレイアウトでは要素はブロックまたはインラインのいずれかになります。Flowレイアウトの詳細については、後ほど解説します。
コンフリクト
1つの要素に複数のレイアウトアルゴリズムを適用した場合、どうなると思いますか? たとえば、次のような場合です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<style> .row { display: flex; } .primary.item { position: absolute; } </style> <ul class="row"> <li class="item"></li> <li class="primary item"></li> <li class="item"></li> </ul> |
リストの3つのアイテムはすべてFlexコンテナ内の子であるため、Flexboxに従って配置されるはずです。しかし、2番目のアイテムはposition: absolute;
を設定することで、Positionedレイアウトを選択しています。
私の理解では、要素はプライマリのレイアウトモードでレンダリングされます。これは詳細度にすこし似ており、特定のレイアウトモードは他のモードよりも高い優先度を持ちます。
正確な階層はわかりませんが、Positionedレイアウトはすべてに優先される傾向があります。したがって、上記の例では、2番目のアイテムにはFlexboxではなく、Positionedレイアウトが使用されます。
その結果、Flexboxの計算では子が3つではなく、2つしかないかのように動作します。Flexboxアルゴリズムに関する限り、この2番目のアイテムは存在しません。アルゴリズムにまったく影響を与えません。
一般的にコンフリクトは通常、かなり明白であり意図的なものです。しかし、ある要素が期待どおりに動作していないことに気づいた場合、その要素がどのレイアウトアルゴリズムを使用しているかを特定することをお勧めします。その答えは、あなたを驚かせるかもしれません。
相対ポジション
ここで難問があります。各要素が単一のレイアウトアルゴリズムでレンダリングされる場合、相対的な位置関係をどのように説明すればよいのでしょうか。
position: relative;
を持つ要素は、明らかにPositionedレイアウトを使用してレンダリングされます。top
やleft
などの排他的なプロパティを使用できます。さらに、Flexbox/Grid レイアウトにも参加できます。
すべての要素は特定のフォーマットコンテキストでレンダリングされ、それに参加するかどうかはレイアウトアルゴリズムに任されています。通常、Positionedレイアウトアルゴリズムはそのようなコンテキストを無視しますが、相対的な配置の例外を作成します。
相対配置された要素がFlexboxコンテキスト内でレンダリングされる場合、Positionedレイアウトアルゴリズムはその要素を参加させるようにします。そのコンテキストを使用してサイズと位置が確立されると、Positionedレイアウトが適用されます(たとえば、top
やleft
で位置を調整します)。
これは構図に少し似ていると考えることができます。Positionedレイアウトアルゴリズムは、相対的に配置された要素に対してFlexboxレイアウトアルゴリズムを構成します。
インライン要素の不思議なスペース
古くからあるCSSで起こる問題を取りあげ、レイアウトアルゴリズムに着目することでどのように解決できるかを見てみましょう。
画像の下になぞのスペースが!
画像の下に少し余分なスペースがあるのはなぜでしょうか?
デベロッパーツールで調べると、数pxのズレがあることに気づくはずです。
デベロッパーツールで確認
画像の高さは250pxですが、コンテナの高さは258.5pxになっています!
CSSのボックスモデルを知っている場合は、padding
, border
, margin
を使用して要素のスペースを調整できることをご存知でしょう。画像にmargin
が、もしくはコンテナにpadding
があると思うかもしれません。
しかしこの場合、これらのプロパティは一切関係ありません。そのため、私は何年も前からこれを「インラインのマジックスペース」と呼んでいます。通常の犯人が原因ではありません。
何が起きているのかを理解するには、Flowレイアウトをもう少し深く掘り下げる必要があります。
Flowレイアウトとは
上記で解説した通り、Flowレイアウトはワープロソフトと同様にドキュメント用に設計されています。ドキュメントの構造は次のとおりです。
- 個々の文字は組み合わさって、単語や文章になります。これらの要素はインラインで水平に並んで配置され、十分な水平スペースがない場合は改行されます。
- 段落は、見出しや画像と同じようにブロックとみなされます。ブロックは上から下に向かって、1つずつ垂直に積み重ねられます。
Flowレイアウトは、この構造に基づいています。個々の要素はインライン要素(段落内の単語のように水平に)、ブロック要素(上から下に積み重ねたレンガのように垂直に)として配置されます。
インライン要素は水平配置、ブロック要素は垂直配置
言語によって異なる方向性
この記事では、英語のような左から右への水平方向の言語を想定しています。しかし、これは一般的ということではありません。
アラビア語やヘブライ語などの言語は水平方向ですが、右から左に書かれます。また、中国語、日本語、韓国語などの言語は、上から下に垂直に書かれます。
このような違いに対応する形でCSSを記述することが一般的になってきています。たとえば、英語では左側のマージンを、アラビア語では右側のマージンを設定するために、margin-inline-start
と記述します。
参考: CSSのボックスモデルにおける古い方法とこれからの方法 -論理プロパティにおける考え方
参考: CSSのmargin-block-start, margin-inline-endなど、論理プロパティの使い方を徹底解説
ほとんどのHTML要素には、実用的なデフォルトのスタイルが設定されています。<p>
や<h1>
はブロックレベル要素とみなされ、<span>
や<strong>
はインライン要素とみなされます。
参考: 各ブラウザごとのデフォルトのスタイルシート、user agent stylesheetのまとめ
インライン要素は、レイアウトの一部としてではなく、段落の途中で使用されることを意図しています。例えば、文章の途中に小さなアイコンを追加したいとします。インライン要素は周囲のテキストの可読性に悪影響を与えないようにするため、垂直方向に少し余分なスペースを追加します。
ここで話を元に戻します。
なぜ画像に余分なスペースがあるのでしょうか? その答えは、画像はデフォルトでインライン要素だからです。
Flowレイアウトアルゴリズムでは、画像を段落の1文字として扱い、(理論上の)次の行の文字に不快なほど近づかないように、下に少しスペースを追加します。
デフォルトでは、インライン要素は「ベースライン」に揃えられます。これは画像の下部が、テキストが置かれている目に見えない水平線に揃えられることを意味します。画像の下にスペースがあるのはそのためで、これはjやpのようなディセンダー(下線)のためのスペースです。
つまり、原因はpadding
でもborder
でもmargin
でもなく、Flowレイアウトがインライン要素に適用するわずかな固有のスペースです。
画像下のスペースを解決する方法
この問題を解決する方法はいくつかあります。 最も簡単なのは、Flowレイアウト内でこの画像をブロックとして扱うことです
display: block;
を与える
この「インラインのマジックスペース」は私のキャリアにおいて何度も遭遇したので、CSSリセットに記述しておくと便利です。
あるいは、この現象はFlowレイアウト固有のものなので、別のレイアウトアルゴリズムに切り替えることもできます。
display: flex;
を与える
最後にレイアウトアルゴリズムはFlowのままで、line-height
で増えたスペースを0にすることもできます。
line-height: 0;
を与える
line-height: 0;
にする方法は、行間を0にすることで行間をすべて削除します。複数行のテキストに適用してしまうと、行がくっついてしまうので、適用するコンテナにテキストが含まれていないことを確認してから使用してください。
解決方法としては、display: block;
またはdisplay: flex;
のいずれかを使用することをお勧めします。line-height: 0;
は問題が行間に起因することを証明するために紹介しただけです。
line-heightとアクセシビリティ
行の高さについて「スタイルなし」のHTMLは、行が近すぎるためにアクセシブルではないと考えられていることをご存知ですか。行の間隔が狭いと、失読症の人にとってテキストを解析するのが難しくなります。
ほとんどのブラウザは、デフォルトのline-height
は1.1〜1.2で出荷されますが、WCAGガイドラインによると、本文テキストに対して少なくとも1.5に設定する必要があります。
私が作成したCustom CSS Resetをチェックして、この問題にどのように取り組むんでいるのかを確認してください。
参考: モダンCSSリセットを徹底解説、最近のデバイス・モダンブラウザの仕様に対応
終わりに
重要なことは、CSSで特定のプロパティが何をするのかだけを勉強していたのでは、この謎のスペースの原因が何なのか理解できません。MDNのdisplay
にもline-height
のページにもこのような解説はありません。
この記事で解説したように、「インラインのマジックスペース」は謎ではありません。Flowレイアウトアルゴリズムで、インライン要素はline-height
の影響を受けるという仕様が原因です。しかし、私のメンタルモデルに大きな穴が開いていたため、謎のように見えました。
CSSには数多くのレイアウトアルゴリズムがあり、それぞれに癖があり、隠れたメカニズムがあります。CSSのプロパティだけに注目してしまうと、それは氷山の一角に過ぎません。コンテキストの積み重ねやブロックの内包、カスケードのオリジンなど、本当に重要なコンセプトを学べません。
残念ながら、ネット上のCSS解説の多くは、同様に浅いものばかりです。ブログの記事やツイートで便利なCSSスニペットが紹介されていても、それがなぜ機能するのか、レイアウトアルゴリズムがそれをどのように使用するのかを説明されていないのはよくあることです。
CSSはデバッグが難しい言語です。エラーメッセージもデバッガーもconsole.logもありません。直感だけが最高のツールです。CSSのスニペットを理解しないまま使い始めると、レイアウトアルゴリズムの隠された側面がわたし達の歯車を狂わせ、動きを止めてしまうのは時間の問題でしょう。
数年前、私はCSSに対する直感力を養い始めることにしました。予期せぬ動作で惑わされたときは、あたかも温かい風呂に入るかのように、その問題に没頭しました。MDNドキュメントやCSSWGの仕様を深く掘り下げ、何が起こっているのかが本当にわかったと思えるまで、コードをいじくり回しました。
これは本当に価値のある投資でしたが、なんということでしょう、とても時間がかかったのです 😅
私は他のデベロッパーのために、このプロセスをスピードアップさせたいと考えています。最近、CSS for JavaScript Developersという包括的なオンラインコースをリリースしました。
このサイトでは、CSSがどのように機能しているのか、その仕組みを探ります。CSSの直感を一度に1つのパズルのピースとして構築するのに役立つ、堅牢なメンタルモデルを提供することに重点を置いています。CSSの課題に二度と直面しないことをお約束することはできませんが、課題を克服するために必要なツールキットを構築するお手伝いはできます。
これまでに、Facebook、Google、Microsoft、Netflixなど、さまざまな企業の9500人以上のデベロッパーがこのコースを受講しています。その反応は圧倒的にポジティブなものでした。
sponsors