文档流与盒模型
- 构建页面布局的通用小贴士
- 设置元素大小的实用经验
- 逻辑属性(logical properties)简介
- 负的外边距与外边距折叠(margin collapsing)
- 保持页面组件间的间距一致
在页面上实现元素布局涉及很多技术。在一个复杂的网站上,可能会用到网格(grids)、绝对定位元素以及其他大小不一的元素。需要掌握的内容太多了,想要学会所有布局相关的技术可能有点不切实际。
本书后续几章将深入研究布局技术。在此之前我们一定要认真打好基础,深刻理解浏览器给元素设置大小和位置的工作原理。更高级的布局话题都是建立在文档流(document flow)和盒模型(box model)等概念之上的;它们都是决定页面元素尺寸和方位的基本规则。
本章将构建一个基本的 单栏页面布局(one-column page layout)。对此您可能再熟悉不过了,因为它是一个经典的 CSS 入门练习;但在跟随我的引导完成该布局后,您会发现其中经常容易被忽视的几个细节。我们将探讨盒模型的一些边界情况,并分享几个有关设置元素大小和对齐方式的实用经验。
3.1 常规文档流
本章将构建一个简单的页面,顶部是标题栏,下方则为内容。内容宽度有限,以免文本行过长。这样的布局由于不存在并排的文本块,通常被称为 单栏布局(one-column layout)。
本章结束时的页面效果如图 3.1 所示。我特意将这个页面设计成“块状”(“blocky”)风格,这样就能很清楚地看到各元素的大小和位置。
图 3.1 本章最终要构建的单栏页面布局效果
新建一个页面和一个空的样式表,并按代码清单 3.1 中的 HTML 代码更新页面。该页面包含一个标题栏(header)和一个占据页面的大部分空间的主容器。容器内的 <main>
元素包含正文内容,而 <aside>
元素则包含一些社交媒体的超链接。
代码清单 3.1 单栏页面布局 HTML 代码
<!doctype html>
<html lang="en-US">
<head>
<link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header class="page-header">
<h1>Franklin Running Club</h1>
</header>
<div class="container">
<main class="main">
<h2>Come join us!</h2>
<p>
The <b>Franklin Running Club</b> meets at 6:00pm every Thursday at the
town square. Runs are three to five miles, at your own pace.
</p>
<p>
Join us while we train for the
<a href="/st-patricks">St. Patrick's Day 5k</a>. Don't forget to wear
green!
</p>
</main>
<aside class="social-links">
<a href="/mastodon" class="button-link">follow us on Mastodon</a>
<a href="/facebook" class="button-link">like us on Facebook</a>
</aside>
</div>
</body>
</html>
先从简单的样式开始:设置页面字体,然后再给页面和每个主容器指定背景色。这样就能在操作过程中查看每个容器的位置和大小。完成后的页面效果如图 3.2 所示。
图 3.2 带背景色的三个主要容器
相关样式代码如代码清单 3.2 所示,将其添加到样式表中。
代码清单 3.2 应用字体及颜色样式
:root {
--brand-color: #0072b0; /* 标题文字颜色设为自定义属性,以便后续复用 */
}
body {
margin: unset; /* 移除用户代理样式添加的外边距,以便内容能到达浏览器窗口的边缘 */
background-color: #eee;
font-family: Helvetica, Arial, sans-serif;
}
.page-header {
color: #fff;
background-color: var(--brand-color);
}
.main {
background-color: #fff;
border-radius: 0.5em;
}
.social-links {
background-color: #fff;
border-radius: 0.5em;
}
在开始调整这些元素尺寸或布局之前,需要注意默认页面布局的行为方式。页面元素通常有两种基本类型:行内(inline) 和 块(block)。每种类型都有各自不同的行为特征。
行内元素 会同页面文字一起从左至右流动。如果这些行内元素到达其容器边缘,就会换行(line wrap)。本例页面中的行内元素包括:“Franklin Running Club” 字样周围的 <b>
元素,以及超链接周围的 <a>
元素。
块元素(通常又称“块级”元素,“block-level” elements)则在各自独立的行上进行显示。它们会自动填满所在容器的宽度。即便通过样式减少块级元素的宽度,其上下两边也会留有一个换行符(line break)。该行为是通过用户代理样式表的样式声明 display: block
实现的,涉及元素包括 <p>
、<div>
和 <header>
等。
该行为也被称为 常规文档流(normal document flow)。
定义
常规文档流是指页面元素的默认布局行为。行内元素与页面文字一起从左至右流动,到达容器边缘时换行;块级元素则独立成行,且上下各带一个换行符。
需要特别注意的是,常规流的高度和宽度 在本质上是不同的。按照常规文档流的设计,宽度是受限的,而高度则不作限制。页面内容会填充其容器的宽度,然后按需换行(line wrap as necessary)。
也就是说,父元素的宽度决定了子元素的宽度,而高度则恰恰相反:子元素的高度决定了父元素的高度。因此需要对宽高分别采用不同的方法来进行操作。本章后续内容就将介绍这些方法。
3.1.1 内容水平居中
构建页面布局时,通常应先从高级 DOM 元素(higher-level DOM elements)入手。应该先确定元素自身的大小和相互间的位置关系,再聚焦处理它们的子元素。这样可以先对页面进行“粗略切割”(“rough cut”),以便后续能更加专注于处理更小的细节。
提示
布局页面最好按由外向内的顺序进行设置。先将较大的容器元素设置就位,再去处理容器内较小的元素。
页面应该对正文区的宽度进行限制。由于块级元素会默认填充其容器宽度,因此对于全宽元素(full-width element),通常无需指定 width: 100% 或 width: 100svw 样式,但通常会让默认宽度小一点。
限宽后的页面效果如图 3.3 所示。注意观察左右两边的浅灰色外边距,还有等宽的标题栏与主容器,以及同时靠左对齐的文字内容。
图 3.3 限制宽度后的页面效果
该布局常用于设置页面内容居中。也可以将内容放在两个嵌套的容器内,然后对内部容器设置外边距,令其位于外部容器的中心(如图 3.4 所示)。Web 开发者 Brad Westfall 将这种居中方案称为 双容器模式(double-container pattern)。
图 3.4 双容器模式下的内容块水平居中示意图
本例中的外部容器即 <body>
元素。默认情况下,其宽度已经是页面宽度的 100%,因此无需指定宽度;而在 <body>
内部,正文内容被包裹在了一个 <div class="container">
元素中(即内部容器)。指定该容器的宽度,并控制好左右外边距即可设置内容居中。
同理,标题栏也可以如法炮制,以便与下方内容保持一致。此时外部容器变为 <header>
元素,而<h1>
成为内部容器。由于宽度必须相同,因此可以使用自定义属性来控制二者的大小。按如下代码更新样式:
代码清单 3.3 双容器样式
:root {
--brand-color: #0072b0;
--column-width: 1080px;
}
.page-header h1 {
max-width: var(--column-width); /* 设置最大宽度 1080px */
margin: 0 auto; /* auto 会让左右外边距自动填充可用空间,从而使元素在外容器内居中 */
}
.container {
max-width: var(--column-width); /* 设置最大宽度 1080px */
margin: 0 auto;
}
通过设置左右外边距为 auto,外边距将根据需要自动扩展,以填充外部容器中可用的剩余宽度。这通常是设置内容块水平居中的最简单的方法。但需要注意的是,在常规文档流中,这种做法对上下边距无效。
使用最大宽度 max-width 来代替宽度 width,可以让元素在屏幕视口窄于 1080px 时同步缩小至 1080px 以下。也就是说,在较小的视口中,内部容器将填满屏幕;而在较大的视口中,它只能最大扩展到 1080px。这样的设计对于避免在尺寸较小的设备上横向滚动屏幕至关重要。本章后续内容将详细介绍 max-width 及其相关属性(properties)。
3.1.2 逻辑属性
过去,文档流的默认行为会给需要翻译成特定语言的网站造成困难。常规流是按从左至右、从上到下的方向流动的,这是因为包括英语在内的绝大多数语言都是这样书写的。但是,为了让“万维”网(“world-wide” web)能够真正名符其实地对全世界开放,就必须兼容书写方式各不相同的其他语言,包括从右向左阅读的语言(如阿拉伯语、希伯来语),以及垂直书写的语言(如日语、繁体中文)。W3C 联盟为此投入了大量工作,最终为 CSS 成功引入了 逻辑属性(logical properties) 的概念。
定义
逻辑属性 为元素流向的处理提供了一种新的思路。它是针对块级元素和行内元素对应的方向而言的,而这些方向又是随不同语言的书写习惯而动态变化的。这样就无需生硬地指定元素上、下、左、右的尺寸以及宽高的具体大小了。
使用逻辑属性时,之前的水平方向与垂直方向的概念,分别被 行内基准方向(即 inline base direction,表示文字在行内的流动方向)和 块级流动方向(即 block flow direction,也就是段落等盒型元素自然堆叠的方向)所取代。要设置的属性不再是 width,而是 inline-size。二者虽然在水平书写模式下效果完全相同,但在垂直书写模式下 inline-size 可用于指定高度;同理,高度 height 也无需设置了,取而代之的是 block-size 属性:它也可以在垂直书写模式下设置宽度。
逻辑属性还将以往的 “上(top)、右(right)、下(bottom)、左(left)” 换成了 开始(start) 和 结束(end)。因此,padding-left 和 padding-right 分别变为 padding-inline-start 和 padding-inline-end;border-top 和 border-bottom 则分别变为 border-block-start 和 border-block-end——这些属性的含义会根据书写模式的不同而重新动态调整。
图 3.5 列举了块级方向与行内方向在各种书写模式下的文档流向。箭头从每个维度方向的“开始(start)”指向“结束(end)”。图中用 border-inline-start 加粗了每个示例中的某一个边框,以强调其行为特征。
图 3.5 行内元素与块级元素在各种书写模式下的文档流方向
要适应这些逻辑属性,关键是得熟悉这些新的叫法。改变原有的页面布局大可不必,换掉那些习以为常的术语即可。至于适应多种书写模式及语言的问题,还是留到真要翻译页面内容的时候再说吧;在使用逻辑属性的前提下,如果布局方案能在从左到右书写的、熟悉的语言中奏效,那么一旦书写模式改变,该布局方案也会随之调整。
工作中只用到一种语言,是否还需要逻辑属性?
这取决于您的工作和生活环境。创建可以支持其他书写模式的网站,这样的场景可能并不多见。即便如此,理解逻辑属性的工作原理仍然非常重要,原因有三——
逻辑属性是 flexbox 布局和 grid 布局中众多核心概念不可或缺的前期储备知识(本书将在后续两章中深入探讨这些布局概念);
一些逻辑属性是没有对应的传统属性的,但有时用起来往往更加方便;
随着逻辑属性在行业中的应用日益广泛,正确理解遇到的新样式写法也尤为重要。
实际工作中,如果正在开发的网站无需切换书写模式,那么究竟是用逻辑属性还是传统属性,亦或是将二者有机结合,都完全可以由您自行决定。即使是在多语言场景下,有时可能也需要用到一些传统属性,例如要让新增的样式效果在任何书写方向上都保持一致的时候。
如果您一直在用浏览器的 DevTools 工具检查页面元素,可能都已经注意到了,用户代理样式表在处理默认外边距时用到了逻辑属性;处理列表元素时也使用了 inline-start 风格的内边距;类似的情况还出现在了其他一些属性中。
在层叠规则中,传统属性及其对应的逻辑属性可以相互覆盖。因此,如果对列表设置了左内边距 padding-left,则会覆盖用户代理样式中的 padding-inline-start 样式;相反,也可以使用 margin-block-start 和优先级更高的选择器来覆盖优先级较低的 margin-top 样式。也就是说,这两种方法可以在样式表中相互交换,并且仍然可以获得可预测的效果。
几乎所有与垂直或水平样式相关的 CSS 属性(property)都有对应的逻辑属性。这样一来,您就可以基于当前语言的具体特征来定义页面布局。以下罗列了部分常见的传统属性和取值、及其对应的逻辑属性的写法和取值情况:
width
/inline-size
height
/block-size
margin-top
/margin-block-start
margin-bottom
/margin-block-end
margin-left
/margin-inline-start
margin-right
/margin-inline-end
text-align: left
/text-align: start
text-align: right
/text-align: end
border-top-left-radius
/border-start-start-radius
border-top-right-radius
/border-start-end-radius
border-bottom-left-radius
/border-end-start-radius
border-bottom-right-radius
/border-end-end-radius
以上列表只展示了一部分情况,但足以清楚演示这种新的写法了。通过使用 block/inline 与 start/end,您所熟悉的传统样式属性写法都可以切换成对应的等效逻辑属性。
关于逻辑属性的全面介绍,请参阅→
3.1.3 用好逻辑属性的简写形式
某些逻辑属性恰巧为常见的样式设置提供了简写形式。例如,margin-inline 可以一次性设置起始(左侧)和结束(右侧)外边距、而无需设置上下两侧的外边距。使用 margin-inline: 2rem 可以将起止外边距(即左外边距与右外边距)同时设为 2rem;或者使用 margin-inline: 2rem 4em 将开始(左)外边距设为 2rem,并将结束(右)外边距设为 4em。同理,margin-block 也可以通过设置块级元素外边距的起止样式来简化传统的上下外边距,类似的属性还包括 padding-inline、padding-block、border-inline 以及 border-block。传统的经典 CSS 属性是没有这样的行为特征的。
这种写法能让之前介绍的双容器模式更加简洁。按照代码清单 3.4 更新样式表,注意代码还包括将 max-width 属性替换为对应的逻辑属性 max-inline-size:
代码清单 3.4 将逻辑属性应用于双容器模式
.page-header h1 {
max-inline-size: var(--column-width); /* max-width 的等效逻辑属性 */
margin-inline: auto; /* 将左右外边距设为 auto,而无需指定上下外边距 */
}
.container {
max-inline-size: var(--column-width); /* max-width 的等效逻辑属性 */
margin-inline: auto; /* 将左右外边距设为 auto,而无需指定上下外边距 */
}
这些更改不会给页面带来任何视觉变化,仅仅是方便了页面样式的编写。我发现这些样式简写非常有用,因为我经常要为外边距或其他样式属性单独指定上下或左右两边的值。
3.2 盒模型
当前示例页需要处理的下一个问题,是主容器和社交链接区域中的内边距。当前这些区域的文字紧挨着白色背景的边缘,应该适当留白看起来才不会那么拥挤,也更方便阅读。按代码清单 3.5 所示代码更新样式。
代码清单 3.5 设置容器内边距
.main {
padding: 1em 1.5rem; /* 为容器添加内边距 */
background-color: #fff;
border-radius: 0.5em;
}
.social-links {
padding: 1em 1.5rem; /* 为容器添加内边距 */
background-color: #fff;
border-radius: 0.5em;
}
现在两个白色容器内的内容区稍微变窄了一些,留出了足够的呼吸空间。然而这样一来,正文的左侧就不再与上方标题栏内的文字在水平方向上对齐了(如图 3.6 所示):
图 3.6 设置内边距后,文字不再统一左对齐
这个问题貌似可以通过在页面标题栏添加类似的内边距来解决;但如果按照代码清单 3.6 更新样式表,会发现情况没有丝毫改变:
代码清单 3.6 给页面标题添加内边距
.page-header h1 {
max-inline-size: var(--column-width);
margin-inline: auto;
padding-inline: 1.5rem; /* 给页面标题设置相同的内边距 */
}
如果恰巧在较小的屏幕上(宽度小于约 1100px)显示,可能看起来像是有效的;但在更宽的屏幕上,根本没有产生任何肉眼可见的变化。即使添加了内边距 padding,标题栏内容区的宽度也没有像主容器里的那样变窄。
这都是 盒模型(box model) 的默认行为造成的。根据盒模型的设计规范,页面上的每个元素都是由四个重叠在一起的矩形所构成的。内容区(content area) 是最里面的矩形,其中包含元素的内容;内边距区(padding area) 包含 内容区 外加 所有内边距部分;同理,边框区(border area) 则是 内边距区 外加 所有边框部分;而 外边距区(margin area) 则是最外面的矩形,包含 边框区 外加 所有外边距部分。
定义
盒模型 描述了 HTML 元素的各个组成部分(内容区、内边距、边框和外边距),以及它们对元素尺寸大小的影响;这些组成要素所产生的各矩形盒将由浏览器完成布局并最终呈现到页面上。
指定元素的高度或宽度,也就设置了该元素内容区的大小;所有的内边距、边框和外边距都会添加到其外部(如图 3.7 所示):
图 3.7 盒模型的默认行为模式
这样的行为模式意味着一个宽度为 300px、内边距为 10px 且边框宽 1px 的元素,渲染出的实际宽度为 322px(即宽度加上左右内边距再加上左右边框)。要是单位再不一致,情况就更令人困惑了。
回到示例页面,给宽度为 1080px 的 <h1>
元素添加内边距,最终增加了其有效宽度。内边距在内容区 1080px 的外部,而正文区的宽度仍保持 1080px。
注意
上下外边距以及上下内边距在行内元素上的行为模式略有不同。这些边距值虽然也会增加元素的高度,但并不会影响到行内元素所在容器的高度;其容器的高度是由行内元素的行高 line-height 决定的。如需变更该行为模式,行内元素须声明 display: inline-block。
Outline 轮廓 —— 另一种边框类型
与边框 border 类似,元素也可以添加一个 outline 轮廓。 其行为模式很像边框,但不会增加元素尺寸,也不是盒模型的组成部分。outline 位于边框外部,与外边距重叠。它既不会改变元素的大小或位置,也不会对页面布局造成任何影响。
与 border 类似,outline 属性也是 outline-color、outline-style 及 outline-width 这三个属性的简写形式。例如,outline: orange solid 2px 会在元素周围添加一个 2px 宽的橙色(orange)轮廓。与边框不同的是,无法为元素的每一侧设置不同的轮廓;所有四条边上的轮廓样式始终相同。在以前,轮廓的四个角总是正方形尖角,但最近部分浏览器已经更改了轮廓的行为模式,以便与元素上任何指定了 border-radius 的圆角曲线相匹配。
想要控制轮廓的位置,可以通过设置 outline-offset 属性来实现。其属性值为正值(如 outline-offset: 3px)时,轮廓将向外扩展,从而增加元素边框与轮廓之间的空间;为负值时,轮廓将向内收缩,使其与元素边框区域重叠。
全面理解盒模型是用好 CSS 的重中之重。内边距(padding)和边框(border)都有可能增加元素的尺寸大小——如果在这一点上认识不到位,CSS 的这些行为模式可能会让您措手不及。想要调整样式与之相适应,首要的一步是弄清为什么会发生这种情况。
3.2.1 避免使用魔数
有时遇到像这样的问题,人们往往会反复试错各种属性值来达到想要的效果,尤其是在用百分比定义大小的时候。
假设样式宽度用的是 70% 而不是 1080px,一个天真的修复办法很可能是减少 <h1>
元素的百分比宽度;或许改成 66% 看上去还行,但这并不可靠。这里的 66% 就是一个 魔数(magic number)。您并没有使用一个理想的值,而是在样式上一顿东拼西凑肆意更改,直到改出想要的效果。
通常情况下,编程中出现魔数的做法并不可取,因为很难解释清楚该魔数为什么有效。如果不了解魔数的出处,自然也就无法预测它在不同情况下表现出的行为。也许文字在 1400px 宽的视口上能对齐,但换到更大或更小的屏幕上就不行了。尽管在开发 CSS 样式时有试错的时候,但那通常是针对与样式本质相关的选择而言的,而不是用于强行调整元素的定位布局。
取代魔数的一个替代方案,是把具体计算的麻烦交给浏览器处理。本例中,<h1>
共超宽了 3em(考虑到左右内边距),因此可以使用 calc() 函数来减少相应的准确宽度。将宽度设置为 calc(var(--columns-width) - 3em) 正好满足需求。但还有一种更好的解决方案。
译注
“魔数”一词在上一版中译为“魔术数值”,但并未对其含义展开讨论。根据《代码大全》第二版第 12 章 12.1 小节的解释,magic number 被译为“神秘数值”,是指在程序中出现的、没有经过解释的数值字面量,如 100 或者 47524。原文摘录如下,以加深理解:
“Magic numbers are literal numbers, such as 100 or 47524, that appear in the middle of a program without explanation.”
这里之所以选用“魔数”,是因为“魔数”更常见,有点约定俗成的意味。
3.2.2 调整盒模型
盒模型的默认行为往往会导致页面元素的大小和对齐出现问题;而人们想要的效果,是希望设置的宽度能包含内边距和边框。在 CSS 中可以通过 box-sizing 属性来调整盒模型的行为。
box-sizing 的默认值为 content-box,也就是说,指定的任何高度或宽度,其实设置的都是内容盒(content box)的尺寸大小。如果将 box-sizing 的属性值改为 border-box,那么属性 width、height、inline-size 和 block-size 设置的尺寸,就是内容区、内边距和边框区域共同组合起来的尺寸,而这正是本例期望的效果。
如图 3.8 所示,左边盒模型的 box-sizing 设为了 border-box。此时内边距不会加宽元素,而是让里面的内容区收窄;高度也是如此。左右两边的元素具有相同的宽度和高度;注意,当存在有效的内边距或边框时,具有边框盒尺寸(border box sizing)的元素将比具有内容盒尺寸(content box sizing)的元素更小。
图 3.8 边框盒尺寸(border-box sizing)改变了盒模型,从而使宽高更容易预测
如果将 <h1>
改为使用边框盒尺寸(border box sizing),其文字内容遍与下方正文区的内容对齐了(如图 3.9 所示):
图 3.9 边框盒尺寸下的文字内容左对齐效果
根据以下代码更新标题栏的盒模型设置:
代码清单 3.7 具有已更正的盒模型的标题
.page-header h1 {
box-sizing: border-box; /* 将盒模型改为边框盒尺寸 */
max-inline-size: var(--column-width);
margin-inline: auto;
padding-inline: 1.5rem;
}
对一级标题设置 box-sizing: border-box 后,其内边距的大小也计入了 1080px 的宽度内。这样标题文字就与下方的正文内容对齐了。
3.2.3 全局设置 border-box
至此,示例中的元素盒模型行为已经变得更加直观了,但其他元素肯定也有同样的问题。如果能一次性解决这个问题,并且适用于所有元素,那就再好不过了,今后就再也不必逐一考虑该如何调整了。利用通用选择器(*
)就能实现这一目标。如代码清单 3.8 所示,该选择器将对页面上的所有元素生效,同时,我还特意加上了一组对页面上所有伪元素也能生效的选择器。将以下这段代码放在示例样式表的顶部:
代码清单 3.8 通用边框框修复
*,
::before,
::after {
box-sizing: border-box; /* 将边框盒尺寸应用到页面上所有的元素及伪元素 */
}
样式生效后,height 和 width 所指定的,将始终是元素的实际高度和宽度,它们将不再受内边距的干扰。
这样,网站上的每个元素都将具有预见性更好的盒模型行为。建议每次开始新网站的开发时,都将代码清单 3.8 里的样式添加到 CSS 中;长远来看,这将省去很多麻烦。然而,对于现有样式表,尤其是已经在默认的内容盒模型下编写了大量样式的情况下,该设置可能也会带来一些新的问题。如果现有项目确实需要加上这段样式代码,请务必仔细检查是否存在任何错误。
注意
在样式表开头附近添加这段代码已是普遍做法了。从现在开始,本书中的每个示例都将假定这段 border-box 设置位于样式表的开头。
3.3 元素的高度
元素的高度(即块的尺寸)处理起来可能会比较麻烦。按照常规流的设计,宽度是受限的,而高度则不受限。因此,容器的高度是由其内容天然决定的,而非容器自身决定。通常情况下,最好避免给元素(尤其对包含大量内容的元素)设置确切的高度,尽管时常会有这样的冲动。
3.3.1 控制溢出行为
当元素高度被设为一个固定值,内容就可能 溢出(overflowing) 容器。当内容在限定区域放不下而被迫渲染到父元素外面时,就会发生这种现象,如图 3.10 所示。文档流不考虑溢出的情况,容器后面的所有内容都会渲染到溢出内容的上面(译注:原书这里并没有配图,源代码中也没有相应页面,因此我手动补了一个截图 3.10.1,以方便理解)。
图 3.10 内容溢出了容器
图 3.10.1 容器后续内容渲染到溢出内容的上方示意图
用 overflow 属性可以控制溢出内容的行为,该属性支持以下 5 个值:
visible(默认值)—— 所有内容都可见,即使溢出容器边缘。
hidden —— 溢出容器内边距边缘的内容将被裁剪而不可见;用户也无法通过正常滚动看到这些内容,但仍能通过 JavaScript 编程的方式来滚动该元素。
clip —— 类似于 hidden ,但通过编程实现滚动的方式也被禁用了。
scroll —— 容器出现滚动条,用户可以滚动查看剩余内容。在某些操作系统中,即便内容都可见,也会出现水平和垂直两种滚动条,只是在这种情况下滚动条会被禁用(置灰)。
auto —— 仅在内容溢出容器时出现滚动条。
图 3.11 展示了内容溢出时四种 overflow 取值下的容器渲染情况。
图 3.11 从左至右 overflow 取值分别为 visible、hidden、scroll、auto 的对比效果
通常情况下,我更倾向使用 auto 而非 scroll,因为在大多数情况下,我并不想让滚动条一直出现,除非万不得已。
水平方向的溢出
除了垂直溢出,内容也可能在水平方向溢出。比如,当在一个很窄的容器中放入一条很长的 URL 地址;或者在一个小型移动设备上查看一个包含很多列的表格的时候。此时适用的溢出规则与垂直溢出时一致。
也可以使用 overflow-x 属性单独控制水平方向的溢出,或者用 overflow-y 来控制垂直溢出。这些属性都支持 overflow 的所有值。但若将 x 和 y 方向显式声明为不同的取值时,一旦其中一个为 hidden,往往会产生不可预知的效果;在这种情况下,应该改用 clip。
使用滚动条时务须谨慎。浏览器已经自带一个滚动条来滚动页面,如果网页内部再嵌套多个滚动区,用户体验可能会很糟糕。如果用户使用鼠标滚轮向下滚动页面,当鼠标到达一个较小的滚动区,滚轮就会停止滚动页面,转而去滚动较小的区域。
3.3.2 百分比高度的备选方案
用百分比来指定高度也有问题。百分比参考的是元素包含块(containing block)的大小,而容器的高度通常是由子元素的高度决定的。这样就产生了循环定义(circular definition),浏览器没法解析,从而会忽略该声明。想让百分比高度生效,就必须给父元素一个确切的高度。
百分比高度的一个有效应用场景是用在绝对定位的元素上,此时元素的尺寸与其容器息息相关。本书后续章节会专门介绍定位相关的知识。
开发者往往会在下面两个应用场景中,因为尝试百分比高度而陷入困境:
第一个场景,是想让容器填满整个屏幕。这时可能需要将 height: 100% 给到某个元素,进而到其容器,并沿着 DOM 树一直向上,直到将它应用到 <body>
和 <html>
元素。其实更好的方案是使用视口相对单位(viewport-relative units,相关内容第二章已有论述),因为此时无非是想要一个 100svh/lvh 的高度效果。
第二个应用场景,是需要创建等高列的时候。页面设计往往需要两个或多个元素并排且具有相同的高度。而让一个元素的尺寸去参照同级别相邻元素的做法,在 CSS 的历史上是一件非常棘手的事。要实现这一需求,以前需要一些非常取巧的手段;而如今只要应用更现代的技术就能轻松搞定。在后续介绍 flexbox 弹性盒布局和 grid 网格布局的相关章节中,我们将一探究竟。
3.3.3 使用 min-height 和 max-height
min-height 与 max-height(以及对应的逻辑属性 min-block-size 与 max-block-size)是两个非常有用的属性(properties)。通过这两个属性设置一个最小或最大值,就能让元素在给定范围内缩放自如,不用再显式定义高度了。
如果要将一张大图放在一大段文字后面,但又担心图片溢出容器,就可以用 min-height 给容器设置一个最小高度,而不必指定确切的高度。这么一来,该容器的高度最小也不会低于设定的值;要是内容过多,浏览器则会让其高度自然升高,以免内容溢出。
如图 3.12 所示的三个元素,左侧的元素没有设置 min-height,因此高度是由内容自然决定的;其余两个元素的 min-height 均设为 3em。对于中间的元素,自然高度虽然不足 3em,但 min-height 将其补到了 3em;而右边的元素内容够多,其高度已然超过了 3em,此时容器则会自然升高来容纳它们。
图 3.12 三个元素、一个没有指定高度、另两个设置了 min-height 为 3em 的效果对比
如果要在容器上设置高度 height,大多数情况下我都会改用 min-height。这样做往往就不必为后续的溢出问题伤脑筋了。
相应地,属性 max-height 则会让元素尺寸自然增大到某个值,一旦达到该值,元素就不再升高,其内容也将随之溢出。类似的属性还有 min-width 和 max-width(以及 min-inline-size 和 max-inline-size),用于限定元素的宽度。
3.4 负的外边距
与内边距和边框宽度不同,外边距可以设为负值。负的外边距有一些特殊用途,例如让元素重叠,或者拉伸到比容器还宽。
负外边距的具体行为取决于设置在元素的哪边,如图 3.13 所示。如果作用于左侧或顶部,则负外边距会分别将元素向左、向上移动,导致元素在文档流中与前面的元素重叠。但如果应用于底部,负外边距则不会移动该元素,而是将其下方的元素拉上来;这和直接给正下方的元素一个负的顶部外边距的效果类似。
图 3.13 负外边距的行为特征
尽管负外边距可用于制造多个元素间的重叠效果,但任何复杂的样式追踪起来都会比较麻烦。通常情况下,更推荐使用定位属性 position 来实现该效果;有关定位的知识将在第 6 章中详细介绍。
警告
通过负的外边距实现的元素重叠效果,可能会让包含交互逻辑的元素挪到其他元素后面,从而导致无法正常点击。
根据元素是行内元素还是块级元素,负的右外边距的行为特征也不尽相同。对于行内元素,其行为模式类似于负的下外边距,会将后续内容往左侧拉动,进而与该元素重叠;但在实践中我从未使用过这种方法。
而对于块级元素,负的右外边距则会向右侧拉动元素边缘,使元素扩宽。这可能导致元素边缘超出容器。
此时若将负的左外边距联系起来,元素的左右两边则会延伸到容器外部,致使宽度超过容器宽度。这就是双容器模式(double container pattern,详见 3.1.1 小节内容)的另一种应用,如图 3.14 所示。
图 3.14 负的左右外边距将块级元素扩展到其容器外部示意图
上述效果的样式代码如代码清单 3.9 所示。该方法可用实现图片的“流布溢出(bleed)”效果,令其宽度大于所在文本列。
代码清单 3.9 负的外边距让图片溢出容器
.container {
max-width: 1080px;
margin-inline: auto;
}
.expanded-child {
margin-inline: -2em; /* 致使元素在两个方向扩宽到容器外部 */
}
注意,该方案也可能导致内容超出浏览器视口。因此在实际应用时要注意样式在小屏幕上仍能正常生效。
3.5 外边距折叠
在回到本章的示例页,有没有发现有些外边距不对劲?标题或容器并没有设置过外边距,但却出现了间距(如图 3.15 所示)。这个间距是哪儿来的呢?
图 3.15 外边距折叠导致的间距
当顶部和(或)底部的外边距相邻时,就会重叠为一个单独的外边距。这种现象称为 折叠(collapsing)。图 3.15 中标题栏下方的空白就是外边距折叠导致的。一起来看看它的工作原理。
3.5.1 文字折叠
外边距折叠主要与包含文本的块级元素间的间距有关。默认情况下,段落元素(<p>
)的上下外边距均为 1em,由用户代理样式表指定。但当两个段落紧挨着排列时,其实际外边距并不会相加得到 2em,而是会相互折叠,只产生 1em 的间距。
可以在示例页的 <main>
元素中看到该折叠效果。元素 <h2>
内的标题(即“Come join us!”)其底部外边距为 0.83em,与下方段落的顶部外边距折叠。二者的外边距分布情况如图 3.16 所示。注意观察它们各自的外边距是怎样在页面上占据相同位置的:
图 3.16 标题(左侧)与段落(右侧)勾勒出的外边距示意图
外边距折叠后的实际大小,等于参与折叠的外边距中较大的一方。本例中,标题栏的底部外边距为 19.92px(字号 24px × 0.83em 外边距),而段落的顶部外边距为 16px(字号 16px × 1em 外边距);二者较大的一方 19.92px 即为实际的外边距大小。
3.5.2 多个外边距折叠
就算两个元素不是相邻的同级元素,外边距也会折叠。例如将段落元素 p 包裹在一个 div 中,如代码清单 3.10 所示,折叠效果也是一样的。在没有任何其他 CSS 干预的情况下,所有相邻的上下外边距都会发生折叠。
代码清单 3.10 用 div 包裹的段落其外边距同样会发生折叠
<main class="main">
<h2>Come join us!</h2>
<div><!-- 即便用另一个 div 包裹起来,段落的外边距仍会折叠 -->
<p>
The Franklin Running club meets at 6:00pm
every Thursday at the town square. Runs
are three to five miles, at your own pace.
</p>
</div>
</main>
本例中,三个不同元素的外边距折叠在了一起:<h2>
的底部外边距、以及 <div>
和 <p>
各自的顶部外边距。这些样式属性的计算值分别为 19.92px、0px 和 16px。因此元素之间最终的间距(space)仍然是 19.92px,即三者中的最大值。就算将段落元素嵌套进多个 div 容器中,最终渲染的效果还是不变——所有的外边距都折叠到一块儿了。
总之,任何相邻元素的上下外边距都会折叠到一起。如果在页面中添加一个空的、无样式的 div 元素(即没有指定高度、边框或内边距),其自身的上下外边距也会折叠。
注意
外边距折叠只针对上下外边距;左右外边距不会折叠。(纵向书写模式下情况则刚好相反,此时会折叠的反倒是左右外边距)。
折叠的外边距就像是“个人安全距离”(个人安全距离)。如果站在公交车站的两个人都觉得保持 3 英尺(译注:1 英尺约合 0.3048 米)的距离很舒服,那他们会很乐意间隔 3 英尺,不必非要间隔 6 英尺才会感到舒适。
这就意味着在设置元素外边距时,不用太在意其上方或下方内容的干扰。如果设置标题的底部外边距为 1.5em,则无论下方元素是 margin-top 为 1em 的 <p>
,还是不带外边距的 <div>
,其间距均为 1.5em(译注:这里假设 1.5em 的计算值更大)。只有当下方的元素需要更多间距时,折叠后的外边距才会增大。
3.5.3 容器外部折叠
三个连续的外边距折叠可能会让您措手不及。如果容器带了背景样式,元素的外边距在容器外部折叠通常会产生不理想的效果。
这就是导致标题下方出现空隙的原因。页面标题是一个 <h1>
元素,用户代理样式设置了 0.67em(即 21.44px)的底部外边距。标题位于无边距的 <header>
中。这两个元素的底部外边距相邻,因此它们会折叠,从而导致标题的底部外边距为 21.44px。同样的情况也出现在两个元素的顶部外边距上。
这样就有点奇怪了。本希望 <header>
的蓝色背景在纵向上更宽一些,这样标题周围就留出了足够的空间;但外边距未必会在预想的位置发生折叠。还好有很多种方法来避免这种情况发生,并且在之前的页面正文区域其实以及处理过该类问题了;注意观察,“Come join us!”字样上方的外边距并没有折叠到容器外面。这是因为容器设置了内边距 padding,如果外边距之间有任何内边距样式,它们就不会折叠。
如果为 header 容器添加上下内边距,其内部元素的外边距就不会折叠到容器外;也就是说,<h1>
的外边距和 <header>
的内边距都会为标题文本提供空间,略显冗余。此时最佳的解决方案可能需要完全去掉 h1 元素的外边距,让容器 header 的内边距来指定所需空间。
图 3.17 为标题栏设置内边距,防止外边距折叠
按照代码清单 3.11 更新样式,会发现标题区和正文区之间不再有间隙。这个问题稍后再来解决。
代码清单 3.11 给标题栏加上内边距样式
当容器上方或下方出现意外的空白,或者文本被压到容器的顶部或底部时,罪魁祸首很可能就是外边距折叠。
对容器应用 overflow: auto(或者非 visible 的值),可以防止内部元素的外边距与容器外边距发生折叠。通常这种方案副作用最小。
在两个外边距间添加边框或内边距,防止外边距折叠。
对于容器为内联块(inline-block)级元素(其元素是浮动的,详见第 12 章)、或者设置了绝对或固定定位的元素(详见第 6 章),外边距不会在容器外部折叠。
使用 Flexbox 弹性盒布局或 Grid 网格布局时,布局内的元素间不会发生外边距折叠(详见第 4 章和第 5 章)。
display 属性为 table-cell 的元素没有外边距,因此不会折叠。这也适用于值为 table-row 的元素以及大部分表格型的样式值,但 table、table-inline 和 table-caption 除外。
上述这些方法很多会改变元素的布局行为,所以除非能产生理想的布局效果,否则不要轻易使用。
3.6 容器内的元素间距问题
图 3.18 间距适中的社交模块最终布局效果
先处理两个社交按钮的样式(译注:赞助商链接后续会添加,这里暂不用管)。页面已经预留了一个 button-link 类,用作 CSS 选择器再好不过了。
由于设计的是链接的通用样式,指定其为块级元素,不仅能让样式充满容器的整个宽度,还能让每个链接按钮独占一行。按以下代码更新页面样式:
代码清单 3.12 设置侧边栏按钮的大小、字体和颜色
.social-links {
max-inline-size: 25em; /* 收窄容器宽度 */
padding: 1em 1.5rem;
background-color: #fff;
border-radius: 0.5em;
}
.button-link {
display: block; /* 块级元素将填满所有可用宽度,并将每个链接独占一行 */
padding: 0.5em;
color: #fff;
background-color: var(--brand-color);
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
链接样式搞定后,还得处理好它们之间的间距。如果不指定外边距,按钮就会直接堆叠在一起,就像现在这样。备选方案有两个:在必然发生外边距折叠的两个社交按钮间,要么分别设置、要么同时设置它们的上下外边距。
然而不管采用哪种方案,都会遇到一个问题:元素外边距与所在容器的内边距将同时生效。例如给两个按钮加上样式 margin-top: 1.5em,最终效果如图 3.19 所示。
图 3.19 按钮的顶部外边距在感官上增大了容器的内边距
此时,第一个按钮新增的顶部外边距令容器顶部间隙过大,与其余三边一对比显得很不协调。
这个问题有很多种解决方案,代码清单 3.13 给出的是一种较简单的版本。利用相邻兄弟组合选择器(+)选中同一父元素下紧邻其他 button-link 后的 button-link 元素。此时所设置的外边距样式只对两个按钮之间的部分生效。
代码清单 3.13 使用相邻兄弟组合样式在按钮间应用外边距
.button-link {
display: block;
padding: 0.5em;
color: #fff;
background-color: var(--brand-color);
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
.button-link + .button-link {
/* 对紧跟某按钮链接的另一按钮链接应用顶部外边距 */
margin-block-start: 1.5em;
}
该方案应该是有效的。如图 3.20 所示,第一个按钮的上边距不见了,容器四周的间距都是一致的。
图 3.20 按钮周围应用了一致的间距
这种写法充分利用了 + 组合选择器,可以推广应用到需要设置容器内元素间隙的场景中。同理,该写法也适用于在一系列行内元素、或行内块级元素之间设置左外边距。相关组合选择器的汇总梳理,详见本书 附录A。
3.6.1 当内容改变时的处理
刚才的做法思路倒没问题,但只要向侧边栏添加更多内容,间距的问题就又出现了。比如按照以下内容,在页面上再加一个友情赞助的超链接,并指定一个样式类 sponsor-link 以便设置新样式。
代码清单 3.14 给侧边栏添加一个不同类型的链接
<aside class="social-links">
<a href="/mastodon" class="button-link">
Follow us on Mastodon
</a>
<a href="/facebook" class="button-link">
Like us on Facebook
</a>
<!-- 给侧边栏添加一个不同类型的链接 -->
<a href="/sponsors" class="sponsor-link">
Become a sponsor
</a>
</aside>
接着,就得为该链接设置样式,但同时还得处理好它与其他按钮之间的间距。图 3.21 是处理间距 之前 的样子。
图 3.21 第二个按钮与底部链接之间缺少间距
新链接的样式如下所示。样式更新后,您可能首先想的是再加个顶部外边距来解决间距问题。先别急,我接下来会给出另一个巧妙的替代方案。
代码清单 3.15 赞助商链接的样式
.sponsor-link {
display: block;
color: var(--brand-color);
font-weight: bold;
text-decoration: none;
}
再加一个顶部外边距固然正确,但鉴于 HTML 改动频繁的沉疴顽疾,没准下个月或来年,该侧边栏中的某些内容就得挪挪位置或者被替换掉,比如把友情赞助链接移到侧边栏的顶部,又或者需要添加一个组件来注册邮箱简讯等。
只要内容一变,相应的边距问题也只能再搞一遍,以确保每块内容之间都能间隔一致,但容器的上下边缘除外。
3.6.2 更通用的解决方案
Web 设计师 Heydon Pickering 曾表示,外边距“就像是在一个物体的某一侧抹了胶水,而你还没想好要不要把它贴到哪儿或者贴到什么东西上”。与其在当前页面反复设置同样的外边距,不如以一种更通用的方式固定写死,使得该间距任由页面结构如何调整都能生效。这就要用到一类特殊的选择器,Pickering 称之为 迟钝的猫头鹰选择器(lobotomized owl selector),因为它长这样:* + *
。关于该选择器的详细用法,Pickering 还特地写了一篇博客 https://alistapart.com/article/axiomatic-css-and-lobotomized-owls/。
该选择器开头是一个可以选中任意元素的通用选择器(*
),接着是一个相邻兄弟组合器(+
),最后是另一个通用选择器。它因形似一只眼神呆滞的猫头鹰而得名。这只做了脑叶切除术的呆头鹰功能上与前面用过的选择器 .social-button + .social-button
差不多。区别在于它不会选中紧跟在其他按钮后的按钮,而是选中紧跟在任意元素后的任意元素。也就是说,它选中的是除首个子元素外所有同一父元素下的子元素。(也可以使用 :not(:first-child) 选择器等效替换)
接下来演示该选择器在设置页面顶部外边距的具体用法,不妨令其与一个新的“stack”类相结合,添加到需要设置子元素纵向间距的任意容器中。最终效果如图 3.22 所示。
图 3.22 所有元素间距一致
按代码列表 3.16 更新样式。这里用到了另一个组合选择器:子组合器(>),让这只“呆萌猫头鹰”(lobotomized owl)只对 stack 类下的直接后代元素生效。这段代码在页面上大有用处。
代码清单 3.16 创建一个 stack 容器来纵向排布各元素间距
.stack > * + * {
/* 指向堆栈中除第一个以外的所有子项 */
margin-block-start: 1.5em;
}
接下来,就可以将 stack 类添加到任何需要设置子元素间距的容器中了。先加到 <body>
上,这样 <header>
和 <div class="container">
之间就隔开了;再加到 <div class="container">
上,这样主内容区和社交链接容器间也有了一致的间距;最后是社交链接容器,这样三个链接元素也统一隔开了。
加上该间距后,整个页面就大功告成了。完整样式如代码清单 3.17 所示:
代码清单 3.17 最终样式表
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
--brand-color: #0072b0;
--column-width: 1080px;
}
body {
margin: unset;
background-color: #eee;
font-family: Helvetica, Arial, sans-serif;
}
.page-header {
color: #fff;
background-color: var(--brand-color);
}
.page-header h1 {
max-inline-size: var(--column-width);
margin: 0 auto;
padding: 1em 1.5rem;
}
.container {
max-inline-size: var(--column-width);
margin-inline: auto;
}
.main {
padding: 1em 1.5rem;
background-color: #fff;
border-radius: 0.5em;
}
.social-links {
max-inline-size: 25em;
padding: 1em 1.5rem;
background-color: #fff;
border-radius: 0.5em;
}
.button-link {
display: block;
padding: 0.5em;
color: #fff;
background-color: var(--brand-color);
text-align: center;
text-decoration: none;
text-transform: uppercase;
}
.sponsor-link {
display: block;
color: var(--brand-color);
font-weight: bold;
text-decoration: none;
}
.stack > * + * {
margin-block-start: 1.5em;
}
3.7 本章小结
在常规文档流中,行内元素并排排布,必要时换行。块级元素各自独占一行,并默认填满其容器的宽度。
逻辑属性指的是元素在文档流中的各边(sides)或尺寸大小,而非其明确方位。
盒模型描述了外边距、边框、内边距以及内容如何定义元素的尺寸大小。使用 box-sizing: border-box 可以让该尺寸大小的行为定义更加直观。
元素的高度是根据其内容动态确定的。明确限制高度可能会导致溢出问题。
元素的外边距可以与其容器外的其他外边距发生折叠,从而导致元素间的间距异常。应用 overflow: auto 可有效处理该问题。
在统一设置块级元素之间的间距方面,呆头鹰选择器不失为一个好用的工具。