2025年01月13日
こんにちは! みなさん、今日もコーディングを楽しんでいますか?
先日、当ブログの「プログラミング」カテゴリーでの、記事の閲覧数を確認する機会があったのですが、「どうしてフィルタが効かないの?」や「{position: absolute;} の、甘く危険な誘惑」といった、「CSSのお悩みあるある」に言及した記事が特に伸びていて、
「やはりフロントエンド開発の皆さんは、今も昔もCSSレイアウトの実現に苦労されているのだなあ」
と(自分を振り返りつつ)しみじみと感じ入りました。
そんな懐かしさに浸りつつ、こんかいはCSSのfloat
と、BFCというものについてお話しします。float
は、人によっては「ああ、昔あった左右にレイアウトを振り分ける指定だね」いう声も聞こえるかもしれませんね。でもこのfloat
、さまざまな経緯を経たうえで、再定義されて今にいたっています。
さあ、一体これまでにどんなできごとが、float
に起こってきたのでしょうか?
※「経緯はいらないから、float
とBFC
についてだけ知りたい」という方は、こちらのリンクからどうぞ。
※こんかいも、長い文章の例として、青空文庫より「ポラーノの広場」(宮沢賢治)を引用しています。またサンプル画像生成サービスとして、Lorem Picsumを利用しています。
はじまり: float = 画像に対する文書の回り込み
じつはもともとfloat
という指定は、段落(<p>
)において、画像(<img>
)を回り込む文書の配置を実現したいという要望を実現するために考案されました。要するに、以下のように「挿絵」を表現するための手段だったのです。
懐かしいレイアウトですね。Webサイトのことを、何でもかんでも「ホームページ」と呼んでいたあの頃を思い出します。しかし、この「文書の回り込み」こそがほんらい想定されていたfloat
の使い道でした。
なお、float
が指定された画像要素のことを、特に浮動要素(ふどうようそ)と呼びます。また浮動要素は「段落の先頭に配置することで、以降の文書の回り込みを規定する」ため、段落の末尾においても効果はありません。
つづいて: clear = 文字は回り込むが、画像や段落同士は交差しない
さて、先ほどのHTMLは(ブラウザにもよりますが)おおむね、このように見えたと思います。
あえて赤い線をつけましたが、実際のところ、左の画像は上の段落、右の画像は下の段落に属しています。このように、段落の開始位置によって絵の位置や段落どうしがが交わっても、文書の回り込みは無関係に、かつ問題なく続いてゆく……というのがfloat
の特筆すべき優れた特徴であるはずです(まさに、それをしたいがために作った仕組みですから!)。
ところが、実際にページを作ってみると、この挙動が直感に反する、あるいは画像と段落との結びつきがわかりにくい、などの否定的な声も挙がりました。例えば下図のように「画像同士を上下に交差させたくない」という意見や……
さらには下図のように「段落同士を交差させたくない」という意見です。
これらの要望をかなえるために生まれたのが、clear
という宣言です。これは「この要素は、直前の浮動要素と交差しない」という意味です。ここでいう「直前」とは、要素の包含関係などを一切考慮せず、純粋に「HTMLの中で、先に記述された要素」になります(いうなれば、これは「浮動コンテキスト」とでも呼ぶべきものかもしれません)。
画像同士を交差させない例
これを実現するスタイリングは、こうなります。
/** 挿絵は全て浮動要素である */
section p img.left {
float: left;
margin-right: 1rem;
}
section p img.right {
float: right;
margin-left: 1rem;
}
/** この要素は、決して「直前の」浮動要素と交差しない */
section p img {
clear: both;
}
浮動要素とした要素、つまりimg {float: left|right;}
に対しimg {clear: both;}
を指定すると、その浮動要素(画像)が連続で出現する限り、「直前の浮動要素(画像)」への回り込みを解除し続けます。これによって、画像同士が交差しなくなるという仕組みです。これで、最初の要望はかなえられました。
一方で、2枚めの画像のような場合は、画像が段落の開始地点より「後ろ」に追いやられるため、文章が段落の途中の画像を回り込むという、変則的な(意図的であれば「スタイリッシュ」な)レイアウトになっています。
段落同士を交差させない例
これを実現するスタイリングは、こうなります。
/** 挿絵は全て浮動要素である */
section p img.left {
float: left;
margin-right: 1rem;
}
section p img.right {
float: right;
margin-left: 1rem;
}
/** この要素は、決して「直前の」浮動要素と交差しない */
section p:has(img.left, img.right) {
clear: both;
}
:has()
は過去の記事で解説した、関係擬似クラスです。つまり、回り込みを解除するための宣言を、浮動要素の親要素であるはずの<p>
に対して行なっていますね。さきに「要素の包含関係などを一切考慮せず」と書いたのは、このことを指しています。
浮動要素とした要素、つまりimg {float: left|right;}
の後に出現するp {clear: both;}
は、直前の浮動要素に対する回り込みを解除し続けます。これによって、次の段落の開始位置が、前の画像と交差しなくなるという仕組みです。これで、二番目の要望もかなえられました!
一方で、1枚めの画像に対する文書のように、段落内の文字数が画像の高さに満たない場合、次の段落が画像と交差しないため、空白の多い(意図的であれば「画像と文書との関係がわかりやすい」)レイアウトになってしまいます。
それから: 1つの段落に、複数の画像を入れたときの float の仕様化
段落の交差とは別に、たとえば童話のHTML化で1つの段落に挿絵を2つ入れてみたり、論文のHTML化で1つの段落に図説を2つ入れてみたり……これも現実の要望としては、よくあることです。そういうわけで、これも仕様化されています。
float
への値としてleft
とright
(現在では “right-to-left” 表記言語への配慮のため、inline-start
およびinline-end
に書き換えるのが望ましいとされています)の2つが指定できますが:
{float: left;}
の画像が段落内に複数存在する場合、それらは出現順に左→右に向かって並べられる。{float: right;}
の画像が段落内に複数存在する場合、それらは出現順に右→左に向かって並べられる。{float: left;}
と{float: right;}
の画像が同じ段落に存在するときは、left
毎、right
毎に、上記のルールにそって並べられる。- このルールによって画像が段落からあふれる場合は、文書の折り返しと同様に、画像を折り返して配置を続ける。
という仕様です。
「折り返しが発生したとき」の配置の意味がわからないんだけど?
通常は1つの段落に、こんな形で挿絵なんか置きませんものね。というわけで、浮動要素の配置のコードサンプルを作ってみました。
スライダーを右に伸ばしてみてください。左から画像が増えていきます。3つまでならまだ理解できるところでしょう。
4つ目が置かれたときには、突然こうなります! もちろんこれも、{float: left;}
です。さて、なぜこうなるか、ちょっと予想してみてください。
では答えです。先ほどの「ルール」の最後を思い出してください。
このルールによって画像が段落からあふれる場合は、文書の折り返しと同様に、画像を折り返して配置を続ける。
画像をこれ以上横並びにすると、段落の表示をはみ出てしまいます。なので、画像を「折り返した」のです。それでも浮動要素のルールは生きているので、折り返しで余った「すきま」に、きゅうくつながらも文書は回り込んでいる……なので、このような見え方になるというわけです。
つぎに5個目、これは{float: right;}
ですが、赤枠の場所に出てきました! どうしてこうなったのでしょう? これも、ちょっと理由を考えてみてください。
予想できたでしょうか? それでは正解。これも先ほどの、このルールによるものです。
このルールによって画像が段落からあふれる場合は、文書の折り返しと同様に、画像を折り返して配置を続ける。
もし、段落の右上隅に5つめの画像を置くと、すでに3つ目の画像と重なってしまいます! つまり、こういうことです。
5つめを配置しようとした段階で、すでに3つ目の画像が配置されています。それにぶつかったり、重なったりすることは、浮動要素の配置ルール違反です。なので、ぶつからない範囲で、せいいっぱいの左上……上の図でいう赤い横線まで、上端を下げて配置されることになったのです。
このように浮動要素は、左右の回り込みとは別に、その段落内において宣言された画像要素の順番が重視されます。その上で、はみ出したり重なったりすることはできず、その代わりに「折り返し」のように位置をどんどん下に下げていくのです。
これは、万が一おかしな浮動要素が設定されたとしても、少なくとも画面表示は行えるようにするためのフォールバック的な側面がありました。ところがこの挙動、予想外の形で脚光を浴びるようになってしまったのです……。
転機: 「これって、横並びレイアウトに使えるんじゃね?」
当初、float
は「段落内の画像を挿絵にするための、回り込み定義」でした。しかしブラウザの実装的には「ブロック要素内の任意の種類の要素に対する回り込み定義」となっていました。
そのうえで、「交差を回避するためのclear
」と「複数の浮動要素の回り込みルール」の挙動を組み合わせることで、いまで言う「フレックスボックス」のような、ブロック要素同士を横並びにレイアウトするためのハックが編み出されました。
……ナビゲーションエリアとフッターエリアの間のカードやボタンは、{display: flex;}
で並べているわけではありません。なんとfloat
とclear
だけで、ここまでできてしまうのです! もちろん、折り返しも自動で行なわれています。
このハックの内容は、
- 左寄せをしたい要素群(カード)に
{float: left;}
を指定することで、直前の要素に自身の左端が接するように寄せ続ける(包含要素からはみ出すときは、仕様として「折り返し」がなされるので問題ない)。 - 包含要素の最後の要素(ボタン)は最後に右寄せしたいので、カード群の後に
{float: right;}
を設定し、右端に寄せて配置する。 - 最後に、この浮動要素群の「寄せ」を解除するため、要素群の直後の「フッターエリア」要素に
{clear: both;}
を指定し、横並びレイアウトを終了させる。
といったものです(正直、このライブコードサンプルを作っている最中、とても懐かしい気持ちになりました)。
float
ほんらいの使い方とはあまりにもかけ離れていますが、フレックスボックスの出現までは、じっさい便利に使えてしまう実用的なハックでした。一時期はこのハックを使ったサイトがあまりにもたくさん作られたので、古参のフロントエンド開発者の中には「float
って、このための機能じゃなかったの?!」と、逆に驚いた方もいるかもしれません。
しかし所詮はハック。同じ横並びレイアウトでも、この方法では表現しきれない状況が出てきました。
混乱: ブロック要素の入れ子の「段落ち」
ブロック要素を入れ子にして、以下のようなレイアウトを作りたいとします。
しかし、どんなにfloat
とclear
を工夫しても、せいぜい、このようにしかならないはずです。
グレーの領域が、浮動領域を包んでくれません。どうしてでしょう?……じつは入れ子になった浮動要素には、ボックスモデルにおける「高さ(height
)」が存在しないのです! この問題はすぐに発覚し、「段落ち」と呼ばれるようになりました。
段落ちの回避には、わたしでも想像できない、さまざまな涙ぐましい努力がなされたといいます。しかし、横並びレイアウトの需要を満たす根本的な解決策としてフレックスボックスが誕生したことをきっかけに、この問題は次第に収束してゆきました。
そして、ついにこのタイミングで「入れ子になった浮動要素に高さが存在しないことは、仕様として適切なのだろうか?」や「そもそも、浮動と包含要素における相互作用の『コンテキスト』が、あいまいなのではないか?」といった議論が開始されました。
ほんらいの「段落内の画像を挿絵にするための、回り込み定義」であるfloat
は、「特定の包含要素における、浮動要素のふるまい」として再定義されることになりました。そして現在、「段落の挿絵」にとどまらず、first-letter
宣言などの「回り込み全般」を制御するために使われるようになったのです。
「回り込み」が、ついに帰ってきました! おかえりなさい!
ここからは、float
を改めてお出迎えするに当たり、再定義された部分、BFC
を中心にご紹介していきます。
ブロック整形コンテキスト (block formatting context / BFC)
ブロック要素の入れ子の回り込みでレイアウトが崩れる、いわゆる段落ちは、実際にはそれ以前からも発生が指摘されていました。たとえば:
濃いグレーのブロック要素内の文章が、タイトルのブロック要素を完全に回り込まない場合、「ここで濃いグレーのブロック要素は終わり」となる一方で、タイトルのブロック要素とその回り込み指定だけは終わらずに続いています。濃いグレーのブロック要素に「高さ」が存在しないために発生する、典型的な段落ちですね。
いまやfloat
は画像だけでなく、すべての要素が対象となりました。なのに入れ子のブロック要素で段落ちの挙動が起こるのは、仕様としてつじつまが合いません! この問題に対し、W3Cは「浮動要素よりも、それを包含する要素の仕様のほうが不十分である」と判断し、新たに「浮動要素を包含する要素の仕様」が再定義されました。これがブロック整形コンテキスト、すなわちBFC
です。
「ブロック整形コンテキスト」の定義
ブロックボックスのレイアウトと、浮動要素が他の要素と相互作用する「領域」を規定するコンテキストです。
(粗訳)
block formatting context または block formatting context root の略。ブロック要素の一種であるが、内側の浮動要素をはみ出させない、外部の浮動要素を侵入させない、「マージンの相殺」を抑制するといった、さまざまな非公式の定義をを行なう。(以下略、強調はブログ執筆者による)
https://drafts.csswg.org/css-display/#bfc
「内側の浮動要素をはみ出させない」……まさに「段落ち」を防ぐ挙動ですね!
要素がこのコンテキストになる条件
後付けの定義にはよくあることですが、条件が非常に多いです。なので、ここでは代表的なものだけを紹介します。以下の条件のうち、いずれかひとつでも満たせば、その要素はブロック整形コンテキストになります
(すべての条件は、 https://developer.mozilla.org/ja/docs/Web/CSS/CSS_display/Block_formatting_context を参照してください)。
- 浮動要素 (浮動要素を包含するブロック要素ではないことに注意!)。
overflow
の値がvisible
以外であるブロック要素({overflow: auto;}
など)。- フレックスアイテムのうち、それ自身が入れ子のフレックスコンテナでも、グリッドでも、テーブルでもない要素。
{display: flow-root;}
が指定されたブロック要素。
ここでご注目いただきたいのは、{display: flow-root;}
です。浮動要素を包含したブロック要素にこのスタイルを当てることで、この「ただのブロック」が「ブロック整形コンテキスト」に生まれ変わります! これはそのブロック要素に対し、明示的に「BFCになれ」という宣言のため、意図しない動作や副作用などは起こりません! この指定は、BFC
の策定の際、新しく作られた宣言です。
その他のメリット
ほかにも、このようなスタイリングができるようになりました。このコードで、試しに{display: flow-root;}
を削除し、<p>
をBFC
で無くしてみてください。かつての実装者たちが、あれほどまで「段落ち」を嫌った理由が、きっと分かると思います。
これはいわゆる「ドロップキャップ(段落の先頭の文字を大きくする)」のスタイリングですが、これはフレックスボックスでは実現できない、float
ならではの強みです。
やっぱり「うっかり作っちゃうコンテキスト」になっちゃった。
とはいえ、いいことばかりでもありません。たとえば{overflow: visible以外;}
。段落ちが発生している包含要素に対し、たとえば{overflow: auto;}
を設定すると、上記の段落ちの例は、このように修正することが出来ます。
なぜこれが「いいことばかりでもない」のか? 上でお話ししたように、そのブロックをBFC
にするという目的を達成するためならば、{display: flow-root;}
を指定するだけでいいはずです。一方で、{overflow: auto;}
は「ブロック要素からはみ出した要素をどう描画するか(スクロールバーを付ける、など)」の指定です。そしてこの指定は、その用途のために使われることがとても多いのです!
サイトのデザインによっては、あえて「段落ち」させることもあるかもしれません。しかしそこで「ついうっかり」{overflow: auto;}
を指定してしまったら、BFC
になることで段落ちが行われなくなってしまいます! これは重ね合わせコンテキスト同様、「原因を突き止めることがとても困難なバグ」を生む原因となります。
くれぐれも、「ついうっかり」このコンテキストを生まないよう、細心の注意を払ってください。
まとめ
こんかいは、誕生から紆余曲折を経て、ようやくほんらいの目的で使えるように帰ってきたfloat
の歴史と、それを可能としたBFC
という仕様についてお話ししました。「フレックスボックスがあるのに、今どきfloat
なんて使わないよ」という人もいらっしゃるかもしれませんが、float
とは包含ブロック内での要素同士の「回り込み」を扱うものであることを、いま一度思い出してください。
「回り込み」は、他の方法で代替することの出来ない、唯一無二の優れた機能です。BFC
という強力な武器を手に入れたいまこそ、挿絵に、図表に、積極的に使っていきましょう!
こんかいのお話は、以上です。それでは、よきコーディングライフを!