在网页上实现元素布局涉及很多技术。在复杂网站上,可能会用到浮动元素、绝对定位元素以及其他各种大小的元素,甚至也会使用较新的CSS特性,比如Flexbox或者网格布局。
在此之前我们要打好基础,深刻理解浏览器是如何设置元素的大小和位置的。高级的布局话题基于文档流和盒模型等概念,这些是决定网页元素的大小和位置的基本规则。
我们将构建一个两列布局的网页。你可能很熟悉这个布局,因为它是一个经典的CSS入门练习,但是在完成这种布局的过程中,有一些布局中经常被忽略的一些细节。
我们会处理盒模型的一些边缘情况,我也会分享一些有关设置元素大小和对齐方式的经验。
另外还会处理CSS中最让人头疼的两个问题:垂直居中和等高列。
一、元素宽度的问题
你需要构建一个简单的网页:上面是网页头部,下面是两列内容,最终效果如图所示。我特意将这个网页设计成“块状”风格,这样就能清楚地看到所有元素的大小和位置。
新建一个网页和一个空的样式表,将其链接到一起。将代码清单中的标记加入到新网页中。该网页有一个头部、一个主元素和一个侧边栏,它们构成了网页的两列,并由一个容器元素包了起来。
<body>
<header>
<h1>青春活力跑步俱乐部</h1>
</header>
<div class="container">
<main class="main">
<h2>快来加入我们吧!</h2>
<p>
青春活力跑步俱乐部每个星期四下午6点在人民广场集合,根据个人运动量每次跑步约3~5公里。
</p>
</main>
<aside class="sidebar">
<div class="widget"></div>
<div class="widget"></div>
</aside>
</div>
</body>
下面处理一些基础样式。给网页设置字体,然后给网页和每个主要容器设置背景色,这样方便看清每个容器的位置和大小。完成这些工作后,网页如图所示。
在一些网站设计中,某些容器的背景色可能是透明的。在这种情况下,可以先暂时给容器设置一个背景色,等实现了容器的大小和位置后再去掉背景色。
body {
background-color: #eee;
font-family: Helvetica, Arial, sans-serif;
}
header {
Color: #fff;
background-color: #0072b0;
border-radius: .5em;
}
main {
/*因为IE有一个bug,它会默认将<main>元素渲染成行内元素,而不是块级元素,所以代码中我们用声明display: block来纠正。*/
display: block;
}
.main {
background-color: #fff;
border-radius: .5em;
}
.sidebar {
/* 给侧边栏加上内边距*/
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
基础样式如代码所示。由于现在侧边栏是空的,默认情况下没有高度,因此我们会给它加上内边距撑出一点高度。其他容器最后也需要内边距,但稍后再处理。现在将以下代码添加到你的样式表。
接下来将两列放到合适的位置。我们首先使用浮动布局,将main和sidebar向左浮动,分别设置70%和30%的宽度。代码如下:
.main {
float:left;
width: 70%;
background-color: #fff;
border-radius: .5em;
}
.sidebar {
float:left;
width: 30%;
/* 给侧边栏加上内边距*/
padding: 1.5em;
background-color: #fff;
border-radius: .5em;
}
结果如图所示,并没有达到理想的效果。
两列并没有并排出现,而是折行显示。虽然将两列宽度设置为70%和30%,但它们总共占据的宽度超过了可用空间的100%,这是因为盒模型的默认行为。
当给一个元素设置宽或高的时候,指定的是内容的宽或高,所有内边距、边框、外边距都是追加到该宽度上的。
这种行为会让一个宽300px、内边距10px、边框1px的元素渲染出来宽322px(宽度加左右内边距再加左右边框)。如果这些值使用不同的单位,情况就会更复杂。
在以上例子中,侧边栏的宽度等于30%宽度加上各1.5em的左右内边距,主容器的宽度只占70%。两列宽度加起来等于100%宽度加上3em。因为放不下,所以两列便折行显示了。
(一)避免魔法值
最笨的方法是减少其中一列(比如侧边栏)的宽度。在我的屏幕上,侧边栏改为宽26%,两列能够并排放下,但是这种方式不可靠。26%是一个魔法值(magic number)。它不是一个理想的值,而是通过改样式试出来的值。
在编程中不推荐魔法值,因为往往难以解释一个魔法值生效的原因。如果不理解这个数值是怎么来的,就不会知道在不同的情况下会产生什么样的结果。
我的屏幕宽1440px,在更小的视口下,侧边栏仍然会换行。虽然CSS中有时确实需要反复试验,但目的是为了得到更好的样式,而不是为了强行将一个元素填入一个位置。
替代魔术数值的一个方法是让浏览器帮忙计算。
在本例中,因为加了内边距,两列的宽度总和超出了3em,所以可以使用calc()函数减去这个值,得到刚好100%的总和。比如设置侧边栏宽度为calc(30% -3em)就能刚好并排放下两列,但是还有更好的解决办法。
(二)调整盒模型
刚才遇到的问题说明默认的盒模型并不符合需求。相反,我们需要让指定的宽度包含内边距和边框。
在CSS中可以使用box-sizing属性调整盒模型的行为。box-sizing的默认值为content-box,这意味任何指定的宽或高都只会设置内容盒子的大小。
将box-sizing设置为border-box后,height和width属性会设置内容、内边距以及边框的大小总和,这刚好符合示例的要求。
如图所示,盒模型的box-sizing为border-box。在这个模型中,内边距不会让一个元素更宽,而是让内部的内容更窄。高度同理。
将这两个元素的box-sizing改为border-box就能在一行显示,不管左右内边距是多少。
按照如下代码修改样式表,调整main和sidebar的盒模型。
.main {
box-sizing: border-box;
float: left;
width: 70%;
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
box-sizing: border-box;
float: left;
width: 30%;
padding: 1.5em;
background-color: #fff;
border-radius: 0.5em;
}
使用box-sizing: border-box后,两个元素加起来正好等于100%宽度。现在因为它们70%和30%的宽度包含内边距,所以一行放得下两列。
(三)全局设置border-box
现在这两个元素的box-sizing更符合预期了,但是以后使用其他元素时还会遇到同样的问题。最好能一次解决,这样以后就不用再想着调整盒模型了。
代码用通用选择器(*)选中了页面上所有元素,并用两个选择器选中了网页的所有伪元素。将这段代码放到你的样式表开头。
*,
::before,
::after {
box-sizing: border-box;
}
加上这段代码,height和width会指定元素的实际宽和高。改变内边距不会影响它们。
但是,如果在网页中使用了带样式的第三方组件,就可能会因此破坏其中一些组件的布局,尤其是当第三方组件在开发CSS的过程中没有考虑到使用者会修改盒模型时。
因为全局设置border-box时使用的通用选择器会选中第三方组件内的每个元素,修改盒模型可能会有问题,所以最终需要写另外的样式将组件内的元素恢复为content-box。
:root {
box-sizing: border-box;
}
*,
::before,
::after {
box-sizing: inherit;
}
盒模型通常不会被继承,但是使用inherit关键字可以强制继承。如下述代码所示,可以在必要时选中第三方组件的顶级容器,将其恢复为content-box。这样组件的内部元素会继承该盒模型。
.third-party-component {
box-sizing: content-box;
}
现在网站上的每个元素都有一个可预测性更好的盒模型了。如果要开发一个新的网站,建议全局设置border-box,因为从长远来看,这会给你省去很多麻烦。
但是如果给已有的样式表加上该全局设置就可能有问题,尤其是当你已基于默认的内容盒模型写了很多样式后。如果非要给已有项目加上这段代码,那么一定要彻底检查一遍看会不会有问题。
从现在开始,每个示例都假设你的样式表开头都修改为了border-box
(四)给列之间加上间隔
通常在列之间加上一个小小的间隔会更好看。这有时可以通过给一列加上内边距来实现,但有时这种方式行不通。
比如在示例中,两列都有背景色或者边框,这就需要将间隔放在两个元素的边框之间。
注意两个白色背景之间的灰色空间。实现这种效果有好几种方式,我们来介绍两种方式。
首先,给其中一列加上外边距,再调整元素的宽度,将多出来的空间减掉。代码从侧边栏的宽度中减掉了1%,将其增加到外边距上,代码如下:
.main {
box-sizing: border-box;
float: left;
width: 70%;
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
box-sizing: border-box;
float: left;
/* 从宽度减少1%,左边距1%*/
width: 29%;
margin-left: 1%;
padding: 1.5em;
background-color: #fff;
border-radius: 0.5em;
}
这段代码的确加上了间隔,但是间隔的宽度由外层容器的宽度决定,百分比是相对于父元素的完整宽度的。如果想用其他单位指定间距呢?
可以用calc()来实现。
可以从宽度中减掉1.5em分给外边距,而不是完整宽度的1%。
.sidebar {
box-sizing: border-box;
float: left;
/* 从宽度减去1.5em*/
width: calc(30%-1.5em);
margin-left: 1.5em;
padding: 1.5em;
background-color: #fff;
border-radius: 0.5em;
}
这种方式不仅能够使用em指定间距,而且能让代码意图更明显。之后再看代码,可能看不出为什么使用29%
,但是从代码中的30% -1.5em
则能提供线索,知道它是基于30%算出来的。
二、元素的高度问题
处理元素高度的方式跟处理宽度不一样。之前对border-box的修改依然适用于高度,而且很有用,但是通常最好避免给元素指定明确的高度。
普通文档流是为限定的宽度和无限的高度设计的。内容会填满视口的宽度,然后在必要的时候折行。因此,容器的高度由内容天然地决定,而不是容器自己决定。
普通文档流——指的是网页元素的默认布局行为。行内元素跟随文字的方向从左到右排列,当到达容器边缘时会换行。块级元素会占据完整的一行,前后都有换行。
(一)控制溢出行为
当明确设置一个元素的高度时,内容可能会溢出容器。当内容在限定区域放不下,渲染到父元素外面时,就会发生这种现象。如图展示了这一现象。文档流不考虑溢出的情况,其容器下方的任何内容都会渲染到溢出内容的上面。
用overflow属性可以控制溢出内容的行为,该属性支持以下4个值。
❑ visible(默认值)——所有内容可见,即使溢出容器边缘。
❑ hidden——溢出容器内边距边缘的内容被裁剪,无法看见。
❑ scroll——容器出现滚动条,用户可以通过滚动查看剩余内容。在一些操作系统上,会出现水平和垂直两种滚动条,即使所有内容都可见(不溢出)。不过,在这种情况下,滚动条不可滚动(置灰)。
❑ auto——只有内容溢出时容器才会出现滚动条。
通常情况下,倾向于使用auto而不是scroll,因为在大多数情况下,人们不希望滚动条一直出现。
请谨慎地使用滚动条。浏览器给网页最外层加上了滚动条,如果网页内部再嵌套滚动区域,用户就会很反感。如果用户使用鼠标滚轮滚动网页,当鼠标到达一个较小的滚动区域,滚轮就会停止滚动网页,转而滚动较小的区域。
(二)百分比高度的备选方案
用百分比指定高度存在问题。百分比参考的是元素容器块的大小,但是容器的高度通常是由子元素的高度决定的。这样会造成死循环,浏览器处理不了,因此它会忽略这个声明。要想让百分比高度生效,必须给父元素明确定义一个高度。
人们使用百分比高度是想让一个容器填满屏幕。不过更好的方式是用视口的相对单位vh,第2章已经介绍过。100vh等于视口的高度。还有一个更常见的用法是创造等高列。这不用百分比也能实现。
1.等高列
等高列的问题从CSS出现就一直困扰着人们。在21世纪初,CSS取代了HTML表格成为布局的主要方式。
当时表格是实现等高列的唯一方式,更具体地说,是不明确指定高度就能实现等高列的唯一方式,虽然可以简单地将所有列设置高度500px或者其他任意值,但是如果要让列自己决定高度,每个元素可能算出来都不一样高,具体高度取决于内容。这个简单的用例就足以让人们抓狂。
为了解决这个问题,诞生了很多有创意的解决方案。随着CSS的演进,出现了伪元素、负外边距等方案。如果你还在用这些复杂的方案,那么是时候改变了。
现代浏览器支持了CSS表格,可以轻松实现等高列,比如IE8+支持display: table, IE10+支持弹性盒子或者Flexbox,都默认支持等高列。
很多常见的设计需要等高列,比如两列布局就是个典型的例子。如果将主列和侧边栏的高度对齐,看起来就会更精致。任意一列的内容增加,两列的高度都会增加,同时保持底部对齐。
当然,你可以给两列随便设置一个高度值,但是应该选择什么值呢?太大了就会在容器底部留下大片空白,太小了内容就会溢出。
最好的办法是让它们自己决定高度,然后扩展较矮的列,让它的高度等于较高的列。下面会演示通过CSS表格和Flexbox两种方式实现这种效果。
2.CSS表格布局
首先,用CSS表格布局替代浮动布局。给容器设置display: table,给每一列设置display:table-cell。
注意:这里没有table-row元素,因为CSS表格不像HTML表格那样必须有行元素。
.container {
/*让容器布局像表格一样*/
display: table;
width: 100%;
}
.main {
/*让列布局像表格的单元格一样*/
display: table-cell;
width: 70%;
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
/*让列布局像表格的单元格一样*/
display: table-cell;
width: 30%;
/*外边距不再生效*/
margin-left: 1.5em;
padding: 1.5em;
background-color: #fff;
border-radius: 0.5em;
}
不像block的元素,默认情况下,显示为table的元素宽度不会扩展到100%,因此需要明确指定宽度。
以上代码已经差不多实现了需求,但是缺少间隔。这是因为外边距并不会作用于table-cell元素,所以要修改代码,让间隔生效。
可以用表格元素的border-spacing属性来定义单元格的间距。该属性接受两个长度值:水平间距和垂直间距。(也可以将这两个长度值指定为同一值。)可以给容器加上border-spacing:1.5em 0
,但这会产生一个特殊的副作用:这个值也会作用于表格的外边缘。这样两列就无法跟头部左右对齐了。
机智的你可能会想到负外边距,但是这需要给整个表格包裹一层新的容器。具体步骤:在表格容器外面包一个元素<div class="wrapper">
,将其左右外边距设置为−1.5em,从而抵消表格容器外侧1.5em的border-spacing。
.wrapper {
/* 添加一个新的包裹元素设置负边距*/
margin-left:-1.5em;
margin-right: -1.5em;
}
.container {
display: table;
width: 100%;
border-spacing: 1.5em 0;
}
正的外边距会将容器的边缘往里推,而负的外边距则会将边缘往外拉。结合border-spacing,两列靠近外侧的边缘跟(包裹元素所在的容器盒子)的边缘对齐了。现在的布局满足了需求:两列等高,1.5em的间距,外边缘跟头部对齐。
很多web开发人员大概听过用HTML表格实现布局并非明智之举。在21世纪初,很多网站设计师使用
<table>
元素实现网站布局,因为它比用浮动布局(当时唯一的替代方案)简单。后来,有许多人强烈反对表格布局,因为它用了无语义的HTML标签。那时表格没有承担内容标签的功能,反而做着本该由CSS负责的布局工作。
3.flexbox布局
我们还可以用Flexbox实现两列等高布局。Flexbox不需要一个额外的div包裹元素,它默认会产生等高的元素。此外也不需要使用负外边距。
将div包裹元素去掉,按照代码更新样式表。如果你还不了解Flexbox,下面有简要介绍。
.container {
display: flex;/* 将容器的display设置为flex */
}
.main {
/*弹性容器内的元素不用指定display或float属性*/
width: 70%;
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
width: 30%;
margin-left: 1.5em; /*跟浮动布局一样外边距可以生效*/
padding: 1.5em;
background-color: #fff;
border-radius: 0.5em;
}
给容器设置display: flex
,它就变成了一个弹性容器(flex container),子元素默认等高。
你可以给子元素设置宽度和外边距,尽管加起来可能超过100%, Flexbox也能妥善处理。以上代码清单渲染出来的样式跟表格布局一样,而且不需要额外包裹元素,CSS也更简单。
警告 除非别无选择,否则不要明确设置元素的高度。先寻找一个替代方案。设置高度一定会导致更复杂的情况。
(三)使用min-height和max-height
接下来介绍的两个是很有用的属性:min-height和max-height。你可以用这两个属性指定最小或最大值,而不是明确定义高度,这样元素就可以在这些界限内自动决定高度。
如果你想要将一张大图放在一大段文字后面,但是担心它溢出容器,就可以用min-height指定一个最小高度,而不指定它的明确高度。这意味着元素至少等于你指定的高度,如果内容太多,浏览器就会允许元素自己扩展高度,以免内容溢出。
如图有三个元素。左边的元素没有min-height,因此它的高度由自身决定,另外两个元素都设置了min-height为3em。中间的元素如果自己决定高度的话应该比现在矮,但是min-height值让它的高度为3em。右边的元素内容多到已经超过3em,容器自然地扩展高度,以容纳内容。
同理,max-height允许元素自然地增高到一个特定界限。如果到达这个界限,元素就不再增高,内容会溢出。还有类似的属性是min-width和max-width,用于限制元素的宽度。
(四)垂直居中内容
CSS另一个让人头疼的问题就是垂直居中。过去有好几种方式实现垂直居中,但是每一种方式都有一定的局限性。在CSS中,回答一个问题的答案通常是“这得看情况”,垂直居中就是如此。
为什么vertical-align不生效?
如果开发人员期望给块级元素设置vertical-align: middle
后,块级元素里的内容就能垂直居中,那么他们通常会失望,因为浏览器会忽略这个声明。 vertical-align声明只会影响行内元素或者table-cell元素。对于行内元素,它控制着该元素跟同一行内其他元素之间的对齐关系。比如,可以用它控制一个行内的图片跟相邻的文字对齐。
对于显示为table-cell的元素,vertical-align控制了内容在单元格内的对齐。如果你的页面用了CSS表格布局,那么可以用vertical-align来实现垂直居中。
一个不好的做法就是,给容器设定一个高度值,然后试图让动态大小的内部元素居中。在实现这种效果时,请尽量交给浏览器来决定高度。
CSS中最简单的垂直居中方法是给容器相等的上下内边距,让容器和内容自行决定自己的高度CSS中最简单的垂直居中方法是给容器相等的上下内边距,让容器和内容自行决定自己的高度。
对应的样式见代码如下:
header {
padding-top: 4em;
padding-bottom: 4em;
color: #fff;
background-color: #0072b0;
border-radius: 0.5em;
}
你可以暂时将这段代码放到样式表,看看它在网页中的样式(之后记得删掉,因为在设计中不是这样)
不管容器里的内容显示为行内、块级或者其他形式,这种方法都有效,但有时我们想给容器设置固定高度,或者无法使用内边距,因为想让容器内另一个子元素靠近容器的顶部或者底部。
这在等高列中也是一个常见的问题,尤其是用浮动布局这种较传统的技术实现时。还好CSS表格和Flexbox能够轻松实现居中。(如果用传统的技术,需要用别的办法处理内容居中。)不同的情况有不同的处理方法,具体参考如下面的附加栏所示。
垂直居中指南
在容器里让内容居中最好的方式是根据特定场景考虑不同因素。做出判断前,先逐个询问自己以下几个问题,直到找到合适的解决办法。
❑ 可以用一个自然高度的容器吗?给容器加上相等的上下内边距让内容居中。
❑ 容器需要指定高度或者避免使用内边距吗?对容器使用display: table-cell和vertical-align: middle。
❑ 可以用Flexbox吗? 如果不需要支持IE9,可以用Flexbox居中内容。
❑ 容器里面的内容只有一行文字吗?设置一个大的行高,让它等于理想的容器高度。这样会让容器高度扩展到能够容纳行高。如果内容不是行内元素,可以设置为inline-block。
❑ 容器和内容的高度都知道吗?将内容绝对定位。(只有当前面提到的方法都无效时才推荐这种方式。)
❑ 不知道内部元素的高度?用绝对定位结合变形(transform)。(还是只有当前面提到的方法都无效时才推荐该方法。)
三、负外边距
不同于内边距和边框宽度,外边距可以设置为负值。负外边距有一些特殊用途,比如让元素重叠或者拉伸到比容器还宽。
负外边距的具体行为取决于设置在元素的哪边,如图所示。如果设置左边或顶部的负外边距,元素就会相应地向左或向上移动,导致元素与它前面的元素重叠,如果设置右边或者底部的负外边距,并不会移动元素,而是将它后面的元素拉过来。给元素底部加上负外边距并不等同于给它下面的元素顶部加上负外边距。
如果不给一个块级元素指定宽度,它会自然地填充容器的宽度。但如果在右边加上负外边距,则会把它拉出容器。如果在左边再加上相等的负外边距,元素的两边都会扩展到容器外面。
这就是为什么可以拉宽前面提到的的表格容器布局,让它填满<body>
的宽度,忽略border-spacing
的影响。
负外边距并不常用,但是在某些场景下很实用,尤其是当创建列布局的时候。不过应当避免频繁使用,不然网页的样式就会失控。
警告如果元素被别的元素遮挡,利用负外边距让元素重叠的做法可能导致元素不可点击。
四、外边距折叠
注意看之前的网页布局,有没有发现有些外边距不对劲?头部和容器明明没有添加任何外边距,为什么它们之间会有间距呢?
当顶部和/或底部的外边距相邻时,就会重叠,产生单个外边距。这种现象被称作折叠。图中头部下方的空白就是由于外边距折叠造成的。下面看看它的工作原理。
(一)文字折叠
外边距折叠的主要原因与包含文字的块之间的间隔相关。
段落(<p>
)默认有1em的上外边距和1em的下外边距。这是用户代理的样式表添加的,但当前后叠放两个段落时,它们的外边距不会相加产生一个2em的间距,而会折叠,只产生1em的间隔。
比如,示例中左列里的文字块就发生了外边距折叠。<h2>
标题(“快来加入我们吧! ”)底部的外边距为0.83em,跟它后面的段落的顶部外边距折叠。如图所示,分别是它们的外边距。注意每个元素的外边距是如何在网页中占据相同位置的。
折叠外边距的大小等于相邻外边距中的最大值。标题的下方有19.92px的外边距(24px字号× 0.83em外边距),段落顶部有16px的外边距(16px字号×1em外边距)。它们中的较大者是19.92px,也就是最终渲染的两个元素之间的间距。
(二)多个外边距折叠
即使两个元素不是相邻的兄弟节点也会产生外边距折叠。即使将这个段落用一个额外的div包裹起来,如代码清单所示,最终视觉结果也一样。在没有其他CSS的影响下,所有相邻的顶部和底部外边距都会折叠。
<main class="main">
<h2>快来加入我们吧!</h2>
<div>
<p>
青春活力跑步俱乐部每个星期四下午6点在人民广场集合,根据个人运动量每次跑步约3~5公里。
</p>
</div>
</main>
有三个不同的外边距折叠到一块了:<h2>
底部的外边距、<div>
顶部的外边距、<p>
顶部的外边距。计算值分别是19.92px、0px、16px。因此最终间隔还是19.92px,也就是三者中最大的值。实际上,即使将段落放在多个div中嵌套,渲染结果都一样:所有的外边距都会折叠到一起。
总之,所有相邻的顶部和底部外边距会折叠到一起。如果在页面中添加一个空的、无样式的div(没有高度、边框和内边距),它自己的顶部和底部外边距就会折叠。
折叠外边距就像“个人空间”。如果在公交车站站着两个人,他们每个人都认为较为舒适的个人空间应为3英尺,那么他们就会乐意间隔3英尺,而不必间隔6英尺才让双方满意。
这也就是说可以给任何元素加上外边距,而不必担心它们前后的元素是什么。如果给标题底部加上1.5em的外边距,那么在标题下面不管是顶部外边距为1em的<p>
元素还是没有顶部外边距的div元素,它们之间的间距都等于1.5em。只有当后面的元素需要更大的空间时,折叠外边距才会大于1.5em。
说明 只有上下外边距会产生折叠,左右外边距不会折叠。
(三)容器外部折叠
三个连续的外边距折叠可能会让你措手不及。如果元素的容器有背景色,元素的外边距在容器外面折叠通常会产生不想要的效果。
再看看图中头部下面的间距。网页标题是<h1>
,用户代理样式表给它底部设置的外边距为0.67em(21.44px)。它的父元素是<header>
,没有设置任何外边距。因为它们的底部外边距相邻,所以会折叠,导致<header>
下方出现了21.44px的外边距。这两个元素顶部的外边距也发生了折叠。
这种现象比较奇怪。在这种情况下,我们希望<h1>
的外边距留在<header>
中,但是外边距不一定在理想的地方折叠。幸运的是,有一些方法可以防止这种现象。实际上你已经在网页的主区域修复过这个问题了。
注意看“快来加入我们吧!”上方的外边距没有在容器外面折叠,这是因为弹性子元素的外边距不会折叠,而这一块刚好用了Flexbox布局。
内边距也能解决这个问题。给头部添加上下内边距,外边距就不会在容器外部折叠。
现在将头部的样式更新,让它如图所示,也加上左右内边距。如代码所示更新你的样式表,但是这样会丢失头部和主内容之间的外边距,我们稍后会解决这个问题。
header {
padding: 1em 1.5em;
color: #fff;
background-color: #0072b0;
border-radius: 0.5em;
}
如下方法可以防止外边距折叠:
❑ 对容器使用overflow: auto(或者非visible的值),防止内部元素的外边距跟容器外部的外边距折叠。这种方式副作用最小。
❑ 在两个外边距之间加上边框或者内边距,防止它们折叠。
❑ 如果容器为浮动元素、内联块、绝对定位或固定定位时,外边距不会在它外面折叠。
❑ 当使用Flexbox布局时,弹性布局内的元素之间不会发生外边距折叠。
❑ 当元素显示为table-cell时不具备外边距属性,因此它们不会折叠。此外还有table-row和大部分其他表格显示类型,但不包括table、table-inline、table-caption。
这些方法中有很多会改变元素的布局行为,除非它们能产生想要的布局,否则不要轻易使用。
五、容器内的元素间距
容器的内边距和内容的外边距之间的相互作用处理起来很棘手。我们给侧边栏加上一些元素,看看会出现什么问题以及如何解决。
最后,介绍一个实用技术来简化这些问题。我们将给侧边栏加上两个跳转到社交媒体页的按钮以及一个不重要的链接。侧边栏最终完成的效果如图所示。
先从两个社交链接入手。如代码清单3-15所示,给侧边栏加上两个链接,用button-link类作为选择器。
<aside class="sidebar">
<a href="/weixin" class="button-link">分享到微信</a>
<a href="/weibo" class="button-link">关注微博</a>
</aside>
接下来,要给按钮加上通用样式。将其设置为块级元素,以便填满容器的宽度,也能够让每个按钮单独一行。
.button-link {
display: block;
padding: 0.5em;
Color: #fff;
background-color: #0090C9;
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
完成了这两个链接的样式后,还需要加上间距。此刻,没有外边距的它们直接上下堆叠在一起。
现在有两个选择:分别或同时指定它们的上下外边距,两个按钮之间会发生外边距折叠。
然而不管选择哪种方式,都会遇到一个问题:侧边栏的内边距需要跟按钮的外边距接触。如果加上margin-top: 1.5em
,最终效果如图所示。
现在容器顶部有了多余的空间。第一个按钮的顶部外边距加上容器顶部的内边距产生的空间大于其余三个方向的留白,显得很不匀称。
.button-link {
display: block;
padding: 0.5em;
color: #fff;
background-color: #0090C9;
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
.button-link+.button-link {
margin-top: 1.5em;
}
这看起来生效了(如图所示)。第一个按钮不再有顶部外边距,按钮四周的间距一致。
(一)如果内容改变了
上述方法让一切如预期,但是如果在侧边栏添加更多内容,则会再次出现间距问题。如代码所示,加上第三个链接,类设为sponsor-link
,这样就可以给链接加上其他样式。
<aside class="sidebar">
<a href="/weixin" class="button-link">分享到微信</a>
<a href="/weibo" class="button-link">关注微博</a>
<a href="/sponsors" class="sponsor-link">赞助一下</a>
</aside>
接下来需要给这个链接添加样式,但是同时还得处理它跟其他按钮的间距。如下是处理之前的样子。
将它们添加到你的样式表。
.sponsor-link {
display: block;
color: #0072b0;
font-weight: bold;
text-decoration: none;
}
你可能想给链接也加一个顶部外边距,且慢,接下来会介绍另一个有趣的方案。
虽然给链接加上顶部外边距也可以实现效果,但是考虑到HTML会频繁改动,也许下个月,也许下一年,总有一天这个侧边栏里有些内容需要移动或者替换。可能赞助商链接会需要移动到侧边栏的顶部,或者需要添加一个注册邮件简报的组件。
每一次改变HTML,都需要考虑这些外边距的问题。你得确保每个元素之间有间距,但是容器的顶部(或底部)没有多余的间距。
(二)更通用的解决方案:猫头鹰选择器
外边距“就像是给一个物体的一侧涂了胶水,而你还没有决定是否要将它贴到某处,或者还没想好要贴到什么东西上”。
不要给网页当前的内容固定外边距,而是应该采取更通用的方式,不管网页结构如何变化都能够生效。
这就是Heydon Pickering所说的迟钝的猫头鹰选择器(lobotomized owl selector)(以下简称猫头鹰选择器),因为它长这样:*+ *。
该选择器开头是一个通用选择器(*),它可以选中所有元素,后面是一个相邻兄弟组合器(+),最后是另一个通用选择器。它因形似一只眼神空洞的猫头鹰而得名。
猫头鹰选择器功能接近此前介绍的选择器:.social-button + .social-button
,但是它不会选中直接跟在其他按钮后面的按钮,而是会选中直接跟在其他元素后面的任何元素。也就是说,它会选中页面上有着相同父级的非第一个子元素。接下来用猫头鹰选择器给页面元素加上顶部外边距。
这样就会给侧边栏的每一个元素加上一致的间距。该选择器还会选中主容器,因为它是头部的相邻兄弟节点,也如你所愿加上了间距。
推荐将body放在选择器的前面,这样该选择器就只能选中body内的元素。如果直接使用猫头鹰选择器,它还会选中<body>
元素,因为它是<head>
元素的相邻兄弟节点。
body * + * {
margin-top: 1.5em;
}
你也许会担心通用选择器(*)的性能问题。在IE6中,这个选择器超级慢,因此开发人员会避免使用它。现在不必担心了,因为现代浏览器都能很好地处理。此外,猫头鹰选择器可能会减少样式表中的选择器数量,因为它在全局范围内处理了大多数元素的间距问题。实际上,如果你的样式表要处理的细节很多的话,猫头鹰选择器的性能可能更好。
猫头鹰选择器的顶部外边距对侧边栏有个副作用。因为侧边栏是主列的相邻兄弟元素,所以它也会有顶部外边距。因此要将其恢复为0,还需要给主列补上内边距。
.main {
width: 70%;
/* 给主列增加内边距 */
padding: 1em 1.5em ;
background-color: #fff;
border-radius: 0.5em;
}
.sidebar {
width: 30%;
margin-left: 1.5em;
/* 移除猫头鹰选择器设置的顶部外边距*/
margin-top: 0;
padding: 1.5em;
background-color: #fff;
border-radius: 0.5em;
}
完成最终样式后,页面如图所示。
这样使用猫头鹰选择器是需要权衡的。它省去了许多的需要设置外边距的地方,但是在某些不想加外边距的地方则需要覆盖。通常只在有并列元素,或者有多列布局时这样使用。有时还需要根据设计,给段落和标题设置特定的外边距。
猫头鹰选择器可能并非是所有项目的正确选择,而且很难添加到已有项目中,因为可能破坏已有布局,不过可以在下次开始做新网站或Web应用程序时考虑使用它。