脳内フィルターで見るCSSレイアウト
CSSを勉強中の方の傾向として、displayやpaddingなどの使い方が分かっていても、レイアウトの仕方でつまずきます。プロパティは知っておく必要はありますが、レイアウトの基本は同じぐらい大切です。
私は、頭の中でレイアウトをいくつかのフィルターを通しながらコーディングしてます。そのレイアウトの描き方を解説します。
(解説をわかりやすくするために細かい部分は省いてます)
div要素の見え方
<div>テキスト</div>
<div>テキスト</div>
HTMLやCSSを書いている方ならお馴染みの 「div要素」 です。
CSSを適用していないdiv要素を見た時にどう見えているでしょう?
ブラウザで見た時は、垂直に並んでいます。
では、なぜ垂直に並ぶのでしょうか?
これを論理的に説明できることが頭の中のフィルターを通してレイアウトを描くコツです。
UAスタイルシートのち、通常フロー
リセットCSSでブラウザの見た目を初期化する時がありますが、本当の意味で初期化されていません。
ブラウザを通さないHTMLの見た目があるとするなら、body要素もdiv要素、span要素も全てのHTMLのdisplayプロパティは「inline」になります。
では、HTMLのdiv要素がなぜ「display: block」なのかというと、 ユーザーエージェントスタイルシート(UAスタイルシート) が適用されているからです。 このUAスタイルシートのおかげで、CSSの仕様通りにdiv要素は「display: block」が適用されてるのです。
ChromeのUAスタイルシート抜粋
html {
display: block
}
/* children of the <head> element all have display:none */
head {
display: none
}
meta {
display: none
}
title {
display: none
}
link {
display: none
}
style {
display: none
}
script {
display: none
}
/* generic block-level elements */
body {
display: block;
margin: 8px
}
body:-webkit-full-page-media {
background-color: rgb(0, 0, 0)
}
p {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1__qem;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
}
div {
display: block
}
displayプロパティの値に「block」「list-item」「table」を持った要素を ブロックレベル要素 といいます。
このブロックレベル要素は、テーブルやflexboxなど使用していない通常のレイアウトフロー(通常フロー)であれば書字方向モードにあわせてブラウザの画面内に並べられます。
書字方向モードの初期値はhorizontal-tbなので、垂直に並ぶわけです。もちろん書字方向モードを縦書きのvertical-rlにすると、通常フローの方向が変わるのでdiv要素は水平に並びます。
ボックスの生成
div要素のレイアウトは、displayプロパティによってブロックレベル要素のスイッチしたら終わりではありません。
displayプロパティから、さらにボックスを生成します。
通常フローにある要素がdisplay: blockであれば、ブロックレベルボックス。
通常フローにある要素がdisplay: inlineであれば、インラインレベルボックスを生成します。
ブラウザのレンダリングは、このボックスの生成によって性質が変化します。そして多くの初心者を惑わすポイントです。
ただCSSでブロックレベル、インラインレベルボックスに変化したからといって、HTMLそのものの意味付けは変わりません。
あくまでレイアウトの性質が変化するだけです。稀に勘違いされる方もいるので補足です。
では、ボックスのレンダリングを見てみましょう。 div要素にbackgroundを指定してみます。
<style>
.a {
background: #1565C0;
color: #fff;
}
<style>
<div class="a">テキスト</div>
div要素が画面いっぱいに広がっているのを確認できます。
そう、なぜか広がってくれるのです。
私がCSSを学び始めた頃に「なぜ何もしていないdiv要素が広がるのか」を検索すると、説明には「display: blockだから」「width: 100%で広がる」と書いてありました。
ただwidth: 100%だと、paddingを含めた時の挙動が異なります。これらの説明は、ずっと的外れな気がしていました。
なぜ広がるかという問いに対して「display: blockだから」は正しいですが、説明不足です。
「UAスタイルシート」と「初期値」によってdiv要素は広がります。
全てのCSSプロパティには、初期値が定義されています。 displayプロパティの初期値は「inline」です。 ただdiv要素やh要素、footer要素などが「display: block」である理由は前述していますが、UAスタイルシートによって「block」になっています。 div要素のUAスタイルシートを見てみましょう。
div {
display: block
}
これだけです。 見えている宣言はこれだけですが、継承とプロパティの初期値によって見えない宣言が多く適用されています。 本当は書いてないですが、以下のような状態です。
div {
display: block
/* 書いてないが、適用されている
width: auto;
min-width: auto;
max-width: none;
height: auto;
min-height: auto;
max-height: none;
position: static;
overflow: visible;
......(たくさん)
*/
}
div要素でコンテンツの広がりに影響がある宣言は、UAスタイルシートの「display: block」。
そして書かれていないけれど自動的に定義されている「width: auto」の2つです。
「display: block」がブロックレベル要素になり、さらに「ブロックレベルボックス」を生成します。 widthプロパティの初期値である「auto」は、img要素やiframe要素などの置換要素以外の通常フローのブロックレベル要素である時に、親のコンテンツ幅にフィットさせるように振る舞います。 これがdiv要素が画面いっぱいに広がる要因です。
ブロックレベル要素とwidth: autoの関係性は、CSSの特性によって簡単に変化します。 先ほどのサンプルに同じブロックレベル要素の「display: table」を当ててみましょう。
<style>
.a {
display: table; /* 追加 */
background: #1565C0;
color: #fff;
}
<style>
<div class="a">テキスト</div>
幅が親のコンテンツ幅ではなく、テキストのコンテンツ幅に変わってしまいました。 何が起きたのでしょうか?
display: blockはブロックレベル要素になり、「ブロックレベルボックス」を生成します。
ただdisplay: tableはブロックレベル要素になり、「ブロックレベルボックス」と「テーブルラッパーボックス」を生成します。
そしてテーブルとなることで、テーブルは内部のコンテンツで幅を決定するのでテキストのサイズの幅に変化しました。
これらはボックスの生成の違いによって、要素にレンダリングの影響が出ました。
一部の見た目に惑わされて他の箇所にも無駄にCSS宣言を記述してしまう方をよく見ます。
そいういう方は、生成されるボックスの違いを理解すると、CSSの記述量を減らすことができます。
CSS Display Module Level 3の 2. Box Layout Modes: the display property の章より生成されるボックスの表があるので、一読しておくことをおすすめします。
無名ボックス
CSSのレイアウトで覚えておきたいレンダリングに 無名ボックス があります。
div要素の中に、span要素を配置したとします。
<div>
<span>テキスト</span>
</div>
そうするとdiv要素は「ブロックボックス」(ブロックレベルボックスとは少し異なる)。
span要素は「行内ボックス」と呼ばれるものになります。
次に先ほどのコードからspan要素をとってみましょう。
div要素に直接テキストが記載されました。
ソースコードのHTMLだけ見ると、div要素のブロックボックスのみになってしまいました。
<div>
テキスト
</div>
しかしボックスの中にはインラインであるテキストが入っているので、ブラウザは本来はない行ボックスを自動で生成します。
この生成したボックスを「無名行内ボックス」といいます。
テキストをリンクの例も見ましょう。
div要素内は、マークアップされていないテキストとa要素に分けられますが、マークアップされていないテキストに無名行内ボックスが生成されます。
<div>
テキスト
<a href="#">テキスト<a>
テキスト
</div>
次にa要素ではなく、p要素ならどうでしょう?
<div>
テキスト
<p>テキスト<p>
テキスト
</div>
p要素にはUAスタイルシートでdisplay: blockが付与されているのでブロックボックスが生成されます。
そしてその前後のマークアップされていないテキストは、「無名ブロックボックス」になります。
そのため、div要素内のテキストは通常フローに則って垂直に並びます。
これらの無名ボックスのポイントは、レンダリングに不都合が起きないようにマークアップを補助する働きをすることです。
この考えはdisplay: blockやinline以外を使う時に、非常に重要です。
display: tableを見てみましょう。
テーブルには本来、テーブルの外枠、行、セルの3つが必要です。
しかし下記のコードはテーブルの外枠の指定のみですが、ブラウザは完全なテーブルとしてレンダリングします。
<style>
.a {
display: table;
}
<style>
<div class="a">テキスト</div>
これも無名ボックスが関係しています。
display: tableによって、ブラウザ側が「これはテーブルだ」と認識したら、テーブルの行とセルを探します。
もしテーブルの行とセルがなければ、無名のテーブルの行とセルを補完して不具合が起こることなく、表示されます。
テーブルは内部コンテンツの幅が影響するレンダリングなので、テーブルセルのテキストを基準にボックスの幅が決定されます。
次にflexboxとgridの例を同時に見ましょう。
<style>
.flex {
display: flex;
justify-content: center;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, 200px);
}
<style>
<div class="flex">テキスト</div>
<div class="grid">
テキスト
<div>テキスト</div>
テキスト
</div>
flexboxとgridともに、テキストが直接記載されていますが、ブラウザのレンダリングは問題なく動作します。これも無名ボックスの影響です。
これも無名ボックスの影響です。flexboxは無名flexアイテム、gridは無名gridアイテムがマークアップされていないテキストの外に生成され、ブラウザに都合のいい形でレンダリングされます。
これはブラウザ側が勝手に決めていることではなく、CSSの仕様として決まっています。
文脈
ボックスの変化は、主にdisplayプロパティよって変化します。
ブロックレベル要素やブロックレベルボックスなどを解説しましたが、「文脈」も念頭に入れておきましょう。
文脈は外部表示型と内部表示型があり、さらに整形文脈の確立や重ね合わせ文脈もあります。
今回は触りしか解説しませんが、文脈の理解はレイアウトの流れを理解する上で重要です。
floatプロパティが頻繁に使用されていた過去の時代では、整形文脈の確立はレイアウトにおいて必須の知識でした。
整形文脈の確立を行うと「独立整形文脈」となり、内部のfloatの要素は確立した整形文脈に収まり、外部のfloatの要素は整形文脈内部へ影響を受けません。また親子間でのmarginの相殺が抑制されます。
現在の独立整形文脈は、このmarginの相殺が発生しないようにするぐらいですが、知っているのと知らないのでは雲泥の差があります。
marginの相殺が分からない方のために簡単に説明すると、通常フローの垂直に並んだブロックレベル要素や入れ子で、marginの打ち消しが発生します。
通常フローのmarginは重なる特性があり、重なった時に数値の大きい値が優先されます。このことを「marginの相殺」といいます。
marginの詳しい内容については、10年前に記事を書いているのでこちらをお読みください。
marginの正しい理解
話は戻り、独立整形文脈になるにはいくつか方法があります。displayプロパティが作用するものもあれば、それ以外のプロパティによって独立整形文脈します。
主な方法は以下の通りです。
- <html>要素の配置
- position: abosolute / fixed
- テーブル関連のdislayプロパティの値
- display: inline-block / flow-root
- visible以外のoverflowの宣言
- flex / gridのコンテナーとアイテム
では例を見ていきましょう。通常フローのブロックレベル要素間でmarginの相殺を発生させました。
親「.parent」と子「.child」で相殺が発生し、子の兄弟間でも相殺が発生しています。
<style>
.parent {
margin: 3em 0;
background: #4FC3F7;
}
.child {
margin: 1em;
background: #5E35B1;
color: #fff;
}
</style>
<div class="parent">
<div class="child">テキスト</div>
<div class="child">テキスト</div>
<div class="child">テキスト</div>
</div>
親要素の「.parent」にdisplay: flow-rootを追加します。 そうすると「.parent」の整形文脈が確立されて、marginの相殺が抑制されます。
<style>
.parent {
display: flow-root;
margin: 3em 0;
background: #4FC3F7;
}
.child {
margin: 1em;
background: #5E35B1;
color: #fff;
}
</style>
<div class="parent">
<div class="child">テキスト</div>
<div class="child">テキスト</div>
<div class="child">テキスト</div>
</div>
「.parent」にdisplay: flow-rootを設定しましたが、「.child」にもflow-rootを追加してみましょう。
<style>
.parent {
display: flow-root;
margin: 3em 0;
background: #4FC3F7;
}
.child {
display: flow-root;
margin: 1em;
background: #5E35B1;
color: #fff;
}
</style>
<div class="parent">
<div class="child">テキスト</div>
<div class="child">テキスト</div>
<div class="child">テキスト</div>
</div>
見た目に変化が起きません。「.child」同士はmarginの相殺が発生しているのです。
独立整形文脈は、ボックスを対象として確立するものです。
「.parent」も整形文脈を確立していますが、その内部は通常フローです。またflow-rootは、ブロックレベル要素です。
そのため子も整形文脈を確立していたとしても、通常フローの子同士はブロックレベル要素なので、margignの相殺の適用範囲内となります。
親子関係の相殺の条件は達成できませんが、子の兄弟同士で相殺の条件が達成されます。
もし兄弟で相殺が発生しないようにするには、相殺の条件から外すだけです。
例えば「inline-box」は相殺が発生しない仕様になっています。真ん中のdiv要素を「inline-box」にしてあげましょう。
<style>
.parent {
display: flow-root;
margin: 3em 0;
background: #4FC3F7;
}
.child {
margin: 1em;
background: #5E35B1;
color: #fff;
}
.child:nth-child(2) {
display: inline-block;
}
</style>
<div class="parent">
<div class="child">テキスト</div>
<div class="child">テキスト</div>
<div class="child">テキスト</div>
<div class="child">テキスト</div>
</div>
2番目の要素は相殺の条件を外したため、一部の兄弟間のmarginが相殺されなくなりました。 ただ3番目と4番目の要素はblockなので、相殺が発生しています。
そもそも親が通常フローだから相殺するのであって、通常フローでなくすれば、子の兄弟間の相殺は発生しません。 親にgirdを与えてみましょう。
<style>
.parent {
display: grid;
margin: 3em 0;
background: #4FC3F7;
}
.child {
margin: 1em;
background: #5E35B1;
color: #fff;
}
</style>
<div class="parent">
<div class="child">テキスト</div>
<div class="child">テキスト</div>
<div class="child">テキスト</div>
</div>
display: gridを追加するだけで、親子間や兄弟間での相殺が発生しなくなりました。
flexboxやgirdを付与されると「flex整形文脈」や「grid整形文脈」を形成し、その子は自動的に独立整形文脈に変化したflex/gridアイテムになります。
通常フローでないflex整形文脈やgrid整形文脈になるため子同士で相殺が発生しなくなり、さらに独立整形文脈である子の中に要素があっても子と孫で相殺が発生しなくなります。
「marginはなんてややこしい仕様なんだ」と思う方もいらっしゃると思います。
しかしmarginの相殺はとても便利な仕様です。ちゃんとmarginとレンダリングされるボックスの仕組みが頭に入っていれば扱うのは容易です。
今回marginの相殺が発生しないようにしたのは、整形文脈が理解しやすいためです。
CSSの宣言を与えることは、文脈を変化させる場合あることを認識しておきましょう。
私が見ているCSSのレイアウト
冒頭でいった「レイアウトをいくつかのフィルターに通す」というのは、ボックスや文脈の流れを捉えながらレイアウトの構築を行うことです。
ボックスや文脈を意識すれば、コンポーネントの微妙なパターンに対して、効率よく対応できるすべを思いつきやすくなります。
また不要なCSS宣言も減りますし、コーティングやブラウザの確認の時間も減ります。
さらにCSSのバグに対して強くなります。経験的にバグの発生は「レイアウトが複雑な場合」に発生することが多いです。
レイアウトが複雑な場合というのは、見た目ではありません。
ボックスや文脈的に複雑という意味です。
例を見ましょう。
下にバーがあり、その中にボタンがあります。HTMLのソースと見た目はシンプルです。
しかしCSSは冗長です。
<style>
.wrap {
position: relative;
display: grid;
grid-template-rows: 1fr auto;
min-height: 100vh;
}
.bar {
border-top: 1px solid #000;
display: table;
position: sticky;
bottom: 0;
height: 84px;
}
.button {
position: absolute;
top: 8px;
right: 8px;
border-radius: 9in;
border: 2px solid;
padding: 8px 24px;
color: #ff3d00;
font-size: 32px;
text-decoration: none;
}
</style>
<div class="wrap">
<div class="content">
content
</div>
<div class="bar">
<a href="#" class="button">Button</a>
</div>
</div>
ここでCSSのブロックや文脈を可視化してみましょう。 下部のバーの中が見た目に反して、通常フロー以外の流れが多すぎです。
特にtableとstickyが指定されている箇所が複雑です。ボタンも絶対配置で固定されています。
そもそもボタンを右側に置きたいだけなら、tableやabsoluteを使用する必要がありません。
別の方法をとるべきです。
これは極端な例として挙げましたが、このようなブロックや文脈が絡み合った状態を「レイアウトが複雑」と捉えています。
私は「見た目」「CSSのコード」「ボックス」「文脈」を頭の中で切り替えながら、レイアウトが複雑にならないように、文脈の流れができるだけスムーズになるようにコーディングしています。
プロパティの特性を理解している前提ですが、このフィルターを頭の中で自由に扱えるようになれば、HTMLとCSSで怖いものはなくなるでしょう。
まとめ
display、大事。