外邊距合併規則

寫在前面
margin的合併規則算是CSS盒模型裏最複雜部分,沒有之一。因爲這部分內容涉及很多不太容易理解的概念,例如clearance(間隙)、normal flow/in-flow(常規流)、BFC(塊格式化上下文)、line box(行框)、inline box(行內框)、bidi(雙向環境)等等

CSS盒模型不只是7項水平屬性 + 7項垂直屬性:


margin
  border
    padding
      width/height

P.S.想起高跟鞋的梗——“不僅有padding,今天還加了margin”

相關的內容至少還包括:

context-box與border-box

padding/margin百分比的計算方式

background與padding/margin/border

margin負值

margin合併

盒模型是視覺格式化模型中的基礎單元,是CSS佈局模型中必不可少的一部分

CSS盒模型描述了一個爲文檔樹中的元素生成的並根據視覺格式化模型進行佈局的矩形框

(引自8 盒模型)

所以,盒模型也是CSS在文檔樹之上建立的第一層抽象,是CSS佈局控制與文檔元素直接關聯的部分。而外邊距合併是直接影響垂直格式化的因素之一,有必要深入理解

一.經典場景
下列例子中,假設UA沒有默認樣式表,未聲明的樣式屬性都依照規範取其初始值

另外,假設UA都是遵守CSS規範的

1.列表項間的外邊距合併


li {
    margin: 8px;
}
那麼列表項之間的間距是多少?

.li-case1 li {
    margin: 8px;
    /* 添個上內邊距 */
    padding-top: 1px;
}

.li-case2 li {
    margin: 8px;
    /* 添個下邊框 */
    border-bottom: 1px solid;
}

在case1和case2中,列表項間距分別是多少?

2.深層嵌套的外邊距合併


/* 縮進表示對應文檔結的構嵌套關係 */
div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
}

這4個嵌套的div渲染結果是什麼樣子?


div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
    /* 添個border */
    border: 1px solid;
}

現在呢?


div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    /* 刪掉min-width, min-height和border */
}

那麼現在呢?

3.帶間隙的外邊距合併


div.container {
border-top: 1px solid;
background: #ccc;
margin-bottom: 60px;
}
/* 縮進表示對應文檔結的構嵌套關係 */
div.float {
float: left;
width: 100px;
height: 50px;
}
div.following-float {
clear: left;
margin-top: 50px;
}
div.following-container {
color: red;
}

紅色文本頂端距.following-float底端的距離是多少?


div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* 縮進表示對應文檔結的構嵌套關係 */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      /* 把50改成49 */
      margin-top: 49px;
  }
div.following-container {
    color: red;
}

現在呢?

再把50改成0和51呢?又分別會出現什麼情況?

P.S.這些問題的答案此刻還是未知的,因爲Demo還沒開始寫;-)那麼就有了足夠的時間容我們認真猜一下

二.合併條件
什麼樣的外邊距會發生合併?

水平外邊距不合並。相鄰的垂直外邊距會合並,除了2種特殊情況:

根元素盒的外邊距不合並

如果一個帶有間隙的元素的上外邊距與下外邊距相鄰,它的外邊距會和緊挨着的兄弟(元素)的相鄰外邊距合併,但合併後不會再和父級塊的下外邊距合併

第1條跳過,對根元素應用外邊距不在情理之中

第2條引入了一個新概念,叫“間隙”,英文名clearance,看樣子與clear屬性有關,實際符合直覺,是指clear屬性導致元素位置移動形成的間隙,見CSS規範9 視覺格式化模型。隱含兩個關鍵點:

具有clear屬性

並且(clear屬性)讓元素位置發生移動了

如果滿足這兩個條件,就說一個元素帶有間隙

注意:如果應用了clear屬性,元素的實際位置不變,比如通過margin-top把元素放到那個位置的,此時元素自身的佈局位置與clear效果位置一樣(即clear屬性沒有帶來額外的空間佔用,所謂的間隙),就不具有間隙。反過來,如果應用clear屬性,導致元素的實際位置發生了變化,即元素上方有一部分空間是clear屬性帶來的,那麼就算帶有間隙

帶有間隙還不夠,還要該元素的上下外邊距相鄰(意味着元素的實際高度爲0,且沒有padding, border),同時滿足的話,這個元素的外邊距合併會受到限制:其外邊距只和緊挨着的兄弟的相鄰外邊距合併,合併後的結果不會再和父級塊的下外邊距發生合併

P.S.到這裏有挑戰經典場景3的入場券了,但還差得很遠

“相鄰”的定義
兩個外邊距在什麼情況纔算“相鄰”?

都屬於流內(in-flow)塊級盒,處於同一個塊格式化上下文

沒有行框(line box),空隙,內邊距和邊框把它們隔開

都屬於垂直相鄰框邊界(vertically-adjacent box edges)

3句話4個新概念,深度優先過一下

流內
流內/流外(in-flow/out-of-flow)是指是否用常規流定位方案來佈局該元素

繼續深度優先,定位方案分3種:

常規流。包括塊格式化、行內格式化和相對定位

浮動。從常規流的位置取出來向左/右移

絕對定位。從常規流中脫離出去,根據其包含塊確定自身位置

元素既沒有浮動(float屬性的應用值爲none),也沒有絕對定位(position屬性的應用值不爲absolute),並且不是根元素,那就按常規流來佈局,就屬於流內元素,否則就是流外元素

塊格式化上下文
浮動,絕對定位的元素,非塊盒的塊容器(例如inline-blocks,table-cells和table-captions),以及’overflow’不爲’visible’的塊盒(當該值已被傳播到視口時除外)會爲其內容建立新的塊格式化上下文

在一個塊格式化上下文中,盒在豎直方向一個接一個地放置,從包含塊的頂部開始。兩個兄弟盒之間的垂直距離由’margin’屬性決定

也就是說,如果沒人建立新的BFC,那麼就處於當前BFC。像JS作用域一樣,默認大家都位於最外層作用域(最外層塊格式化上下文),遇到普通塊級盒就放進塊格式化上下文,遇到特殊的(浮動,絕對定位的等等)就新建一層作用域(建立新的塊格式化上下文),它裏面的元素都放進這個內層作用域(新的塊格式化上下文)

佈局完成後從格式化上下文的角度來看,就是一系列嵌套的BFC,每個BFC負責管理一組塊盒(或者說塊級元素)的佈局

注意:這裏不提行內格式化上下文,因爲區分出不同的行內格式化上下文沒有太大意義(規範定義中,沒有關於跨行內格式化上下文的特殊場景)。那麼,什麼時候會創建新的行內格式化上下文?,根據規範,只在塊容器只含有行內級盒時才創建一個新的行內格式化上下文,不像BFC可以顯式地強制創建

P.S.關於何時會創建新行內格式化上下文的更多討論,請查看When does a box establish an inline formatting context?

行框
包含來自同一行的盒的矩形區域叫做行框

一個行框總是足夠高,能夠容納它包含的所有盒。

行框是CSS對行的抽象表示,每行元素都處於同一個行框裏。如果太長放不下出現自動換行,那麼就會爲下一行再創建一個行框。另一方面,行框不是純粹的抽象定義,它具有寬度和高度,用於決定行佈局

相鄰外邊距之間“沒有行框”可以簡單理解爲沒有行內元素把它們隔開

垂直相鄰框邊界

下列4種場景滿足外邊距都屬於垂直相鄰框邊界的情況:

盒的上外邊距與其第一個流內(in-flow)孩子的上外邊距

盒的下外邊距與其下一個流內緊挨着的兄弟的上外邊距

最後一個流內孩子的下外邊距與其height計算值爲’auto’的父元素的下外邊距

盒的上外邊距和下外邊距,要求該盒沒有建立新的塊格式化上下文,並且’min-height’計算值爲0,’height’計算值爲0或’auto’,還沒有流內孩子

看起來太長,我們簡化條件,假設都是流內元素的話,那麼:

父子:父元素上外邊距與長子上外邊距

兄弟:元素的下外邊距與右兄弟的上外邊距

父子:幺兒的下外邊距與父元素的下外邊距

自身:0高“真空”元素的上外邊距與下外邊距

P.S.這裏的“真空”是指——把薯片抽成真空。要麼裏面什麼都沒有,要麼流內孩子都被抽離了

也就是說,“相鄰外邊距”的位置定義具體分3種情況:父子,兄弟和自身(自身上下外邊距合併是比較奇特的)

重新理解“相鄰”與外邊距合併
有了前面的概念鋪墊,現在我們把零散的點整合起來,先重新定義“相鄰”:

父子,兄弟或元素自身的外邊距緊挨在一起就是“相鄰”

還有一個關鍵點:緊挨。就是說這兩個外邊距沒被“牆”隔開,“牆”分3種:

種族:雙方必須都是流內塊級盒

信仰:處於同一個塊格式化上下文

地域:二者之間沒有行框(line box),空隙,內邊距和邊框

到這裏,“相鄰”已經很清楚了,我們再反推外邊距合併的定義:

非根元素的相鄰垂直外邊距會合並,帶有間隙的話,合併受限

受限是指帶有間隙元素自身上下邊距相鄰的話,只能與兄弟元素的外邊距合併,無法和父元素的下外邊距合併

三.合併條件推論
根據外邊距合併的發生條件,有8條推論:

浮動的盒與任何其它盒之間的外邊距不會合並(甚至一個浮動盒與它的流內子級之間也不會)

建立了新的塊格式化上下文的元素(例如,浮動盒與’overflow’不爲’visible’的元素)的外邊距不會與它們的流內孩子合併

絕對定位的盒的外邊距不會合並(甚至與它們的流內孩子也不會)

內聯塊盒的外邊距不會合並(甚至與它們的流內孩子也不會)

流內塊級元素的下外邊距總會與它的下一個流內塊級兄弟的上外邊距合併,除非該兄弟(元素)具有間隙

流內塊級元素的上外邊距會與它的第一個流內塊級孩子的上外邊距合併,條件是該元素沒有上邊框和上內邊距,並且其孩子不具有間隙

一個’height’爲’auto’並且’min-height’爲0的流內塊級盒的下外邊距會與它的最後一個流內塊級孩子的下外邊距合併,條件是該盒沒有下內邊距和下邊框,並且其孩子的下外邊距沒有與具有間隙的上外邊距合併

盒自身的外邊距也會合並,條件是’min-height’屬性爲0,既沒有上下邊框,也沒有上下內邊距,’height’爲0或’auto’,且不含行框的話,那麼其所有流內孩子的外邊距(如果存在的話)都會合並

簡化總結,不過4條:

非流內(絕對定位或浮動)不合並

觸發新BFC創建(浮動,絕對定位元素,非塊盒的塊容器以及’overflow’不爲’visible’的某些塊盒)不與孩子合併

非塊級盒(內聯塊)不合並

一般情況下,兄弟元素的下上外邊距,父子元素的上外邊距、下外邊距,元素自身的上下外邊距會合並

前3點針對“相鄰”的前提條件(流內,同BFC,塊級盒),第4點是對4種“相鄰”場景的轉述,展開就是8條推論

四.合併行爲
兩個相鄰外邊距發生合併後,形成的外邊距叫摺疊外邊距

P.S.collapsed margin故意譯作摺疊表示結果,與合併的動作區分開

外邊距合併有2個特點:

遞歸:即深層合併。合併一次後,再檢查與合併結果相鄰的外邊距有沒有能合併的,有的話接着合

貪婪:追求最寬合併結果。兩個margin正值取最大值,兩個負值取絕對值的最大值

對於遞歸特性,“相鄰”的定義擴展出一條遞歸公式:

摺疊外邊距也能與另一個外邊距相鄰,只要其外邊距的任意一部分與那個外邊距相鄰就算

貪婪與外邊距合併結果計算方式有關,因爲margin允許負值,情況稍微複雜一點:

都是正值,直接求二者最大值

一正一負,相加求和

都是負值,求二者絕對值的最大值

例如:


ul {margin-bottom: -15px;}
  /* 縮進表示對應文檔結的構嵌套關係 */
  li {margin-bottom: 20px;}
h1 {margin-top: -18px;}

那麼h1與最後一個li的垂直距離爲20 + -max(|-15|, |-18|) = 2px

無論對正值還是負值,求最大值的原則都是讓合併結果儘量寬(絕對值更大的負值能讓元素內容偏移出去更遠的距離),即貪婪性

五.在線Demo

Demo地址:http://ayqy.net/temp/margin-collapse.html

P.S.答案都在Demo裏,解釋都在源碼裏

參考資料
When does a box establish an inline formatting context?:問題評論很有價值,有助於理解行內格式化上下文

Margin collapse and clearance:clearance的示例

帶有間隙的外邊距合併示例:要用Firefox看,因爲Chrome和Safari不遵守規範

Collapsing Margins:看了本文就不用看這個了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章