🤼‍♂️

css marginのキモい仕様を暴力的に理解する

2024/04/25に公開

この記事を書こうと思ったきっかけ

最近、長らく遠ざかっていたweb開発に復帰した。
数年ぶりなので色々調べながらcssの仕様について喪失した記憶を取り戻していたが、marginの突き抜けについて誤った内容の記事が最上位に来ているのが気になった。

???「変なブログなんてアテにせず、ちゃんとmdnの仕様書読めよ」
それはそう。
けど正直「たかがmarginごときを理解するのにあんな回りくどい解説読む気しないよ…」と思ってしまうタイプの人間なので、同類に向けてくだけた解説をしてみる。

ちなみに、marginがらみの悩み事から解放されたいだけであれば、flexとgapという素晴らしいテクノロジーがあるのでこれを使うとよい。
※flexとgapについてはzennで他の方が解説してくれているので割愛する

「今さらmarginなんて使わねーし、別に理解できなくてもいいや」と思いつつ、「なんでmarginってあんなキモイ?」という気持ちが心のどこかに引っかかって、CSSヘイトの一部を形成している人にこの記事を贈ります。

marginの覚え方

marginを最初に習う時、こう教わると思う。
「marginは、要素のborderの外側の余白を指定するものですよ。」
「似たような概念で、paddingは、要素のborderの内側の余白を指定するものですよ。」

これは正しい。
正しいが、あまりに簡潔な説明なせいで、この一行がmarginの仕様の全てだと勘違いしてしまう。

多少初学者を混乱させたとしても、後々のイライラを防ぐためにこう書いておくべきだと思う。
「marginは、同レベルなやつらとスペースを巡ってどつき合うためのプロパティですよ。」
「paddingは、絶対に子どもに触れるなっていう意思表示のプロパティですよ。」
「子が親を殴るのはご法度なので、marginは親に対して効きませんよ。」
と。
じゃあ親は子を殴っていいんですか?

個人的な感想だけど、ほとんどの人にとってCSSの挫折ポイントって、本当の取っ掛かり部分ではなくて、そこから一歩進んだ先のdisplay周りなんじゃないだろうか?
だからdisplay周りで挫折しないように、すべてのプロパティについて、最初から親・兄弟要素との関係性とセットで紹介された方が良いと思う。

marginの相殺の覚え方

marginを「同レベルでどつき合うためのプロパティ」だと覚えておけば、相殺は一瞬で覚えられる。
要は腕の長さがmarginの大きさなのだから、腕が長い方のパンチが当たる所まで距離が詰まる。

「余白」と聞くと、平和な余ったスペースの譲り合いのイメージをするかもしれないが、CSSの世界では余白は戦って勝ち取るものだ。

というか、余白に限らず「とにかくその場で一番強いやつの意見が妨げられないこと」が大事にされており、あやふやだったり弱い要素は容赦なく食われる(=上書きされる)世界。
それがCSS。

子のmarginが親を突き抜ける現象

さて、記事のきっかけになったこっちの事象はどうだろう?
こんなサンプルコードを用意してみた。

このコードにはmarginについて直感的にキモいと感じる所が3つほどある。

  1. 子(組織B構成員1)のtop方向marginが親に設定されたmarginのような挙動をしている
  2. 子(組織B構成員3)のbottom方向marginが無視されているように見える
  3. 子要素が親を右方向に突き破っている

原因の1つは、「divにはデフォルトだと高さ(height)の指定が無いこと」だ。

高さが指定されていない親要素(=緑の箱)は、子どもたちを包み込むように同じ高さまで縮んでくる。
要素の「本体」というのはborderの中のことを指すので、親のpaddingも0だと親と子はborderを接することになる。
ほら、抱きしめるには体と体を密着させないといけないでしょう?

子はtop marginを持っているが、これは「同レベルでどつき合うためのプロパティ」なので、振りかざした拳は家族の外(つまり組織A)に向けられる。
親は殴ってはいけないが、赤の他人は殴ってよい。それがCSS的世界観である。

このように考えると、キモ挙動のうち1と2は腹落ちするはずだ。
なお、親にpaddingが設定されていた場合は、少し事情が変わる。
paddingは、「絶対に子どもに触れるなという意思表示のプロパティ」なので、paddingが設定されていると親は子どもの腕(=margin)にも触らないようになる。

最後に、解決していないキモ挙動ついて。
最初にキモ挙動は3つだと言ったが、実は4つある。
上の説明を読んで「おや?」と思った方もいるかもしれない。
「もし"marginは親に対して無効"というのが本当なら、left方向のmarginが親に対して距離を取っているように見えるのはなんでだろう?」と。

この答えは、「見えざる敵」と「横方向だから」ということになる。
親divのdisplay方法はblockなので、同ライン上に兄弟要素はいない。
しかし、CSSの世界において、「子は親の言うことは聞くが、親の事情は知ったことではない」ので、左に"いるかもしれない"奴を殴りつけて50px距離を取ろうとする。

ここで、top方向のmarginと同じ理屈なら、親divも左端から50px離れることになるだろう。
そうならないのは、親のdisplayがblockであり、block要素は潜在的に100%の幅を持っているからだ。

サンプルコードではwidth:300pxと明示しているが、それでも潜在的にwidthは100%なんだ。
そうじゃないと「親divにmargin: 0 auto;をつけて中央揃えする」みたいなテクニックは使えないことになってしまう。

親div(厳密にはそれを読んだブラウザ)は、自分の左に誰もいないことを知っているから、子どもが腕を振り回していようが関係ない。
何も知らない顔で所定の位置に描画される。

子どもは親の事情を知らないから、とにかくleft marginの分だけ左端から離れようとする。
そして左を殴りつけた反動で親の右枠をぶち破っていく。
その結果がサンプルのような状態を生むというわけ。
(親の左端に対して何かした訳ではないから、親の右ボーダーにも無影響という感覚…これが大事)

おわりに

独特なmarginの理屈とCSSの世界観について、完全な初心者でも理解できる…とは行かなかったかもだが、初級者くらいの方のモヤモヤが少しでも晴れていれば幸いだ。

別言語だが個人的に触っているflutterの描画ロジックは、CSSとはまた違った論理で構築されておりCSSのノリで挑むと混乱したりする。
ただ、親・兄弟要素を意識しつつ「誰が何を関心ごとにしているか?」を考える癖をつけておけば理解が速まる点は共通しているので、そういう観点で各言語の描画ロジックを説明するドキュメントが増えるといいなと思う。

とりあえず原因説明を避けてコピペプログラマーを量産するようなブログよりは、この記事が伸びることを祈りつつこの記事はここで終わる。

ご意見・ご指摘があればコメント欄かtwitterまでお願いします。

追記

marginって排他的経済水域じゃん(FUKASE)

Discussion