CSS 包含块和 html&body 间的趣事儿

铺垫

前几天有位小学弟问了我一个问题:“如何才能在页面上创建一个覆盖整个页面的遮罩层?”,解决方案并非重点,小学弟紧接着又提了一个问题:“我给htmlbody同时设置 100% 的高度,再创建一个绝对定位并且宽高 100% 的div做遮罩层,会有什么问题吗?”,我当时的回答是内容过多时会出现问题,但验证之后发现还有一些更加有趣的事儿。

“诡异”的现象

我们先让内容超过一页出现滚动条,并且把htmlbody标签的高度设置为 100%,然后滚动到页面底部。为了方便查看两个元素的大小,分别给html加上红色边框以及给body加上蓝色边框:

<!DOCTYPE html>  
<html lang="en">  
<head>  
  <meta charset="UTF-8">
  <title>html/body test</title>
  <style>
    html, body{
      margin: 0;
      height: 100%;
    }
    html{
      border: 5px solid #E7383E;
    }
    body{
      border: 5px solid #37C0ED;
    }
  </style>
</head>  
<body>  
  <p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p><p>14</p><p>15</p><p>16</p><p>17</p><p>18</p><p>19</p><p>20</p>
</body>  
</html>  

效果是:

说好的高度 100% 呢,怎么变成这幅半吊子样儿了?现在我们把html的高度 100% 去掉:

<style>  
  html, body{
    margin: 0;
  }
  html{
    border: 5px solid #E7383E;
  }
  body{
    height: 100%;
    border: 5px solid #37C0ED;
  }
</style>  

咦,这下好像正常了:

这到底是怎么回事呢?在解释之前,我们必须先了解一个概念——CSS 包含块。

CSS 包含块(Containing block)

根据 W3C 的定义,包含块是指计算元素位置和大小时需要参考的一个非实体矩形框,要想正确地推断出一个元素位置和大小的计算方式,那么判断该元素的包含块是一个必经的步骤。我们不妨通过一个小故事来理解一下包含块的判断方式以及包含块到底干啥用的:

李大爷在北京有一套四合院儿,但是两个儿子都没啥出息,混了好几十年都没能在北京买套房,哪怕是四环的也好啊,所以几个儿子到今天都还和李大爷一起住在这四合院儿里,老大住东房,老二住西房。

正巧老二家上个月又诞下一男孩儿,连上大儿子家里得有四口人了,嘟嚷着房子不够大想改大点儿,李大爷很喜欢这大胖小子,想想咱家院子还挺大,得咧那咱把西房给往院子里扩建一下吧。

老大的女儿下个月就要出嫁了,女儿马上就要离开自己身边了,心中万分伤感,不由地感叹“女大不中留啊”。不过听说女婿混得还不错,在四环边儿上买了一套 90 平的房子,女儿能幸福他这个做爹的也心满意足了。

老二的儿子就没那么争气了,交了个女朋友但又买不起房,所以结婚房子的问题只能去求助爷爷了,得咧,爷爷还是喜欢儿子的,顾不得房子变成丑八怪,把自己的主房又叠了一层楼出来给孙子当婚房用了。

李大爷看着这四合院儿还能解决如此多的问题,望着天空细语:“老爹啊,还好您给我留了个房子,不然今天咱李家可就麻烦咯”。

这个故事告诉我们:李大爷家一直在啃老。但这不是重点,在这个故事中:

  • 四合院儿的地皮也就是浏览器窗口(viewport
  • 李大爷作为一家之主是html标签,也是整个DOM的根元素
  • 老大老二都是某个不知名的div
  • 老大的女儿就是position: fixed的子元素,马上要脱离这个家即脱离了文档流
  • 老二的儿子就是position: absolute,自己的位置和大小都取决于一家之主李大爷的态度

那么他们的包含块分别是:

  • 李大爷作为根元素,上面再无其他,所以李大爷的包含块是一个虚无缥缈的名叫“初始包含块”的东西,而这个“初始包含块”依然是要受到四合院儿即浏览器窗口的限制
  • 老大、老二都是文档流之中的元素,他们的包含块就是离自己最近的父级并且具备话语权的父级元素(block/inline-block/table-cell
  • 老大的女儿因为要嫁人已经脱离文档流了,但我们只说四合院的事儿,所以她即使脱离文档流,包含块也依然是四合院儿外层的元素——浏览器的视图区域
  • 老二的儿子因为结婚其实也脱离文档流了,但他和老大的女儿不一样,他只是 absolute,包含块取决于离自己最近的一个有改造房子权利的父级(position值不是static的元素),如果这个父级是行内元素的话,还得根据direction属性来判断具体的定位效果;如果根本找不到这个父级,那么包含块也是“初始包含块”。而在这个案例中,很明显包含块是指向李大爷了,因为李大爷给他腾了个位置结婚嘛!

把判断步骤用流程图(图片来源:w3help.org)体现则是:

Containing block flow

“诡异”现象的解释

知道包含块是啥后,此前那个诡异现象的原因就可以解释了:由于html标签是根元素,所以它的包含块是“初始包含块”,它的尺寸和大小会受浏览器视窗(viewport)的限制,所以给html标签设置 height: 100%;也就意味着将html的高度设置为浏览器窗口的高度;由于body只是普通的static元素,它的包含块是最近的block/inline-block/table-cell元素即html标签,所以给body设置height: 100%;也就意味着body的高度等于html的高度等于浏览器窗口的高度。

综上,那个诡异的现象只不过是父元素高度不足导致的内容溢出而已。但仅仅到此为止吗?

更“诡异”的现象

我们在最开始的代码上,给htmlbody标签加上背景颜色,为了便于识别,一个用半透明的蓝色,一个用半透明的红色:

  <style>
    html, body{
      margin: 0;
      height: 100%;
    }
    html{
      border: 5px solid #E7383E;
      background-color: rgba(0,126,255,0.3);
    }
    body{
      border: 5px solid #37C0ED;
      background-color: rgba(255,0,0,0.3);
    }
  </style>

效果如下:

结果很奇妙:在浏览器窗口高度内的背景是很奇怪的颜色,说明是htmlbody的背景色叠加了;在浏览器窗口高度以外的背景是蓝色,说明html的背景色生效了body的背景色没有生效,真的没有生效吗?尝试只留下body的背景色来验证一下这个猜想:

<style>  
  html, body{
    margin: 0;
    height: 100%;
  }
  html{
    border: 5px solid #E7383E;
  }
  body{
    border: 5px solid #37C0ED;
    background-color: rgba(255,0,0,0.3);
  }
</style>  

body的背景色依然能够生效!但说好的高度 100% 呢?边框都已经体现了容器的大小了,怎么背景色还能很任性地超出?

翻阅 W3C 中 CSS 背景色说明中的特殊元素的背景色部分后发现有如下信息:

The background of the root element becomes the background of the canvas and its background painting area extends to cover the entire canvas.

也就是说根元素即html元素的背景绘制区域会覆盖整个内容区域。而对body元素的说明则在该条的下方也有进行说明:

The used values of that BODY element's background properties are their initial values, and the propagated values are treated as if they were specified on the root element.

也就是说body元素的背景色设定会和根元素一样被处理为覆盖整个内容区域。并且该文档还建议将背景设置到body元素上而非html元素上:

It is recommended that authors of HTML documents specify the canvas background for the BODY element rather than theHTML element.

总结

诡异的现象解释清楚之后,其实也没有什么可总结的内容了,那么就回归到最开始的问题吧:“如何才能在页面上创建一个覆盖整个页面的遮罩层?”。这样推断下来这个问题最好的答案便是让作为遮罩层的div的包含块指向html或者body,然后给它设置宽高 100%,这样一来无论内容有多高,该遮罩层总是能覆盖所有内容区域了:

<style>  
  html{
    position: relative;
    min-height: 100%; /* 对页面内容不足一屏的情况做处理 */
  }
  .overlay{
    position: absolute;
    z-index: 1000;
    top: 0;
    left: 0;
    width: 100%; /* 或使用等效的 right: 0; */
    height: 100%; /* 以及 bottom: 0; */
    background-color: rgba(0,0,0,.3);
  }
</style>  

版权声明:署名-非商业性使用-禁止演绎(CC BY-NC-ND)

 许可证:创意共享 4.0 许可证

SHARE