Skip to content

定位和层叠上下文

position 属性可以用来构建下拉菜单、模态框以及现代Web应用程序的一些基本效果。

定位可能变得很复杂。许多开发人员对它只有粗略的理解。
如果不完全了解定位以及它可能带来的后果,就很容易给自己挖坑。
有时候你可能会把错误的元素放在其他元素前面,要解决这个问题却没有那么简单。

当看完各种类型的定位后,你肯定能准确地理解每种类型的行为。

我们还会介绍层叠上下文,它属于定位的一个隐藏的副作用。
理解层叠上下文能帮你避免不必要的麻烦,当你被一个页面布局绕进去了,它还能帮你找到问题的突破口。

position 属性的初始值是 static(静态定位)。
如果把它改成其他值,我们就说元素就被定位了。而如果元素使用了静态定位,那么就说它未被定位

前面介绍的布局方法是用各种操作来控制文档流的行为。
定位则不同:它将元素彻底从文档流中移走。
它允许你将元素放在屏幕的任意位置。
还可以将一个元素放在另一个元素的前面或后面,彼此重叠。

固定定位

固定定位不如其他定位类型用得普遍,但它是最好理解的一种定位类型,因此先从它开始介绍。
给一个元素设置 position: fixed 就能将元素放在视口的任意位置。
这需要搭配四种属性一起使用:top、right、bottom 和 left。
这些属性的值决定了固定定位的元素与浏览器视口边缘的距离。
比如,top: 3em 表示元素的上边缘距离视口顶部 3em

设置这四个值还隐式地定义了元素的宽高。
比如指定 left: 2em; right: 2em 表示元素的左边缘距离视口左边 2em,右边缘距离视口右边 2em。
因此元素的宽度等于视口总宽度减去 4em。topbottom 和视口高度也是这样的关系。

用固定定位创建一个模态框

我们要用这些属性创建一个如图7-1所示的模态框。
该模态框会在网页内容前弹出来,它会挡住网页内容,直到关闭该弹窗。


图7-1 一个模态框盒子

通常情况下,模态框用于要求用户阅读一些内容或者在下一步操作之前输入一些内容。
比如,图7-1的模态框展示了一个表单,用户可以注册一个时事通讯。
初始状态下用 display: none 隐藏弹窗,然后用 JavaScript 将 display 改成 block 以显示弹窗。

创建一个新的页面,将代码清单7-1加到 <body> 元素中。
这段代码将所有内容放在两个容器元素中,同时用一个 <script> 标签放置 JavaScript 代码提供基础功能。

代码清单7-1 创建一个模态框盒子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<body>
  <header class="top-banner">
    <div class="top-banner-inner">
      <p>Find out what's going on at Wombat Coffee each
        month. Sign up for our newsletter:
        <button id="open">Sign up</button>
      </p>
    </div>
  </header>
  <div class="modal" id="modal">
    <div class="modal-backdrop"></div>
    <div class="modal-body">
      <button class="modal-close" id="close">close</button>
      <h2>Wombat Newsletter</h2>
      <p>Sign up for our monthly newsletter. No spam.
         We promise!</p>
      <form>
        <p>
          <label for="email">Email address:</label>
          <input type="text" name="email"/>
        </p>
        <p><button type="submit">Submit</button></p>
      </form>
    </div>
  </div>

<script type="text/javascript">
    var button = document.getElementById('open');
    var close = document.getElementById('close');
    var modal = document.getElementById('modal');

    button.addEventListener('click', function(event) {
        event.preventDefault();
        modal.style.display = 'block';
    });

    close.addEventListener('click', function(event) {
        event.preventDefault();
        modal.style.display = 'none';
    });
</script>
</body>

代码清单7-1里的第一个元素是顶部条。它包含了触发模态框的按钮。
第二个元素是模态框。它包括一个空的 modal-backdrop,用来遮住页面剩余部分,将用户的注意力集中到弹窗的内容。
弹窗内容在 modal-body 里。

CSS如代码清单7-2所示,将其添加到你的样式表。它包含了顶部条和模态框的样式。

代码清单7-2 添加模态框样式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
body {
    font-family: Helvetica, Arial, sans-serif;
    /* 设置网页高度,让页面出现滚动条(只是为了演示) */
    min-height: 200vh;
    margin: 0;
}

button {
    padding: .5em .7em;
    border: 1px solid #8d8d8d;
    background-color: white;
    font-size: 1em;
}

.top-banner {
    padding: 1em 0;
    background-color: #ffd698;
}

.top-banner-inner {
    width: 80%;
    max-width: 1000px;
    margin: 0 auto;
}

.modal {
    /* 默认隐藏模态框,当要打开模态框的时候,JavaScript 会设置 display: block */
    display: none;
}

/* 当打开模态框时,用半透明的蒙层遮挡网页剩余内容 */
.modal-backdrop {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.5);
}

/* 给模态框主体定位 */
.modal-body {
    position: fixed;
    top: 3em;
    bottom: 3em;
    right: 20%;
    left: 20%;
    padding: 2em 3em;
    background-color: white;
    /* 允许模态框主体在需要时滚动 */
    overflow: auto;
}

.modal-close {
    cursor: pointer;
}

在这段CSS里,我们使用了两次固定定位。
第一次是 modal-backdrop,四个方向都设置为 0。这让蒙层填满整个视口。
它还有一个背景色 rgba(0, 0, 0, 0.5)。这个颜色符号指定了红、绿、蓝的值均为0,算出来是黑色。第四个值是“alpha”通道,它指定透明度:0是完全透明,1是完全不透明。0.5是半透明,因此该元素下面所有的网页内容就会变暗。

第二次固定定位了 modal-body。它的四条边都在视口内:顶边和底边到视口对应的边缘为 3em,左边和右边距离视口对应的边缘为 20%。
因为它的背景色为白色,所以模态框呈现为一个在屏幕居中的白色盒子。
虽然可以随意滚动网页,但是背景和模态框主体都不会动。

打开页面,我们看到屏幕上方有一个带按钮的淡黄色顶部条。点击按钮打开定位的模态框。
因为是固定定位,所以即使滚动页面,模态框的位置也不会变(为了演示,我们特地将 body 上的 min-height 值设得很大,撑出了滚动条)。

点击模态框顶部的 Close 按钮,关闭弹窗。这个按钮现在的位置不太对,我们稍后会调整它的位置。

控制定位元素的大小

定位一个元素时,不要求指定四个方向的值,可以只指定需要的方向值,然后用 width 和 / 或 height 来决定它的大小,也可以让元素本身来决定大小。请看如下声明。

1
2
3
4
position: fixed;
top: 1em;
right: 1em;
width: 20%;

这段代码会将元素放在距离视口顶部和右边 1em 的位置,宽度为视口宽度的 20%。它省略了 bottom 和 height 属性,元素的高度由自身的内容决定。
例如,这可以用于将一个导航菜单固定到屏幕上。即使用户滚动网页内容,该元素的位置也不会改变。

因为固定元素从文档流中移除了,所以它不再影响页面其他元素的位置。
别的元素会跟随正常文档流,就像固定元素不存在一样。
也就是说它们通常会在固定元素下面排列,视觉上被遮挡。
这对于模态框来说没问题,因为我们希望模态框出现在最前面的中间位置,直到用户关闭它。

而对于其他固定元素,比如侧边导航栏,就需要注意不要让其他内容出现在它下面。
通常给其他内容加一个外边距就能解决该问题。
比如,将所有内容放在容器里,容器设置 right-margin: 20%
外边距会流到固定元素下面,内容就不会跟导航栏重叠。

绝对定位

固定定位让元素相对视口定位,此时视口被称作元素的包含块(containing block)。
声明 left:2em 则将定位元素的左边放在距包含块左侧 2em 处。

绝对定位的行为也是如此,只是它的包含块不一样。
绝对定位不是相对视口,而是相对最近的祖先定位元素。
跟固定元素一样,属性 top、right、bottomleft 决定了元素的边缘在包含块里的位置。

让 Close 按钮绝对定位

为了演示绝对定位,我们重新设置Close按钮的位置,将其放在模态框的右上角,如图7-2所示。


图7-2 Close按钮位于模态框的右上角

我们需要将Close按钮设置为绝对定位。
因为它的父元素 modal-body 是固定定位的,所以会成为Close按钮的包含块。
根据代码清单7-3,编辑按钮的样式。

代码清单7-3 绝对定位的Close按钮

1
2
3
4
5
6
7
.modal-close {
    position: absolute;
    top: 0.3em;
    right: 0.3em;
    padding: 0.3em;
    cursor: pointer;
}

这段代码将按钮放在距离 modal-body 顶部 0.3em、右侧 0.3em 的位置。
通常情况下,就像本例一样,包含块是元素的父元素。
如果父元素未被定位,那么浏览器会沿着DOM树往上找它的祖父、曾祖父,直到找到一个定位元素,用它作为包含块。

说:
明如果祖先元素都没有定位,那么绝对定位的元素会基于初始包含块(initial containing block)来定位。
初始包含块跟视口一样大,固定在网页的顶部。

定位伪元素

Close 按钮已经定位好了,只是过于简陋。
对于这种 Close 按钮,用户通常期望看到一个类似于 x 的图形化显示,如图7-3所示。


图7-3 将 Close 按钮改成 x

你可能首先想到将按钮里的文字close换成 x,但是这会导致可访问性的问题:辅助的屏幕阅读器会读按钮里的文字。
因此要给这个按钮一些有意义的提示。在使用CSS之前,HTML本身必须有意义。

相反,你可以用 CSS 隐藏 close,并显示 x。总共需要两步。
首先将按钮的文字挤到外面,并隐藏溢出内容。
然后将按钮的 ::after 伪元素的 content 属性设置为 x,并让伪元素绝对定位到按钮中间。按照代码清单7-4更新按钮样式。

提示:
相比字母 x,我更推荐用乘法符号的 Unicode 字符。它更对称,也更好看。
HTML 字符 &times; 可以显示为这个字符,但在 CSS 的 content 属性里,必须写成转义的 Unicode 数字:\00D7

代码清单7-4 用一个 × 替换 Close 按钮

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.modal-close {
    position: absolute;
    top: 0.3em;
    right: 0.3em;
    padding: 0.3em;
    cursor: pointer;
    font-size: 2em;
    /* 让链接变成一个小方形 */
    height: 1em;
    width: 1em;
    /* 让元素里的文字溢出并隐藏 */
    text-indent: 10em;
    overflow: hidden;
    border: 0;
}

.modal-close::after {
    position: absolute;
    line-height: 0.5;
    top: 0.2em;
    left: 0.1em;
    text-indent: 0;
    content: '\0007';
}

以上代码清单明确指定按钮为 1em 大小的方形。
text-indent 属性将文字推到右边,溢出元素。它的确切值不重要,只要大于按钮宽度即可。
由于 text-indent 是继承属性,需要在伪类元素选择器上设为 0,因此 x 便不会缩进。

伪类元素现在是绝对定位。因为它表现得像按钮的子元素一样,所以定位的按钮就成为其伪元素的包含块。
设置一个较小的line-height让伪元素不要太高,用top和left属性让它在按钮中间定位。
这里的精确值是我反复试出来的,建议你在自己的浏览器开发者工具里试试,看看它们如何影响定位。

绝对定位是定位类型里的重量级选手。它经常跟JavaScript配合,用于弹出菜单、工具提示以及消息盒子。
我们将用绝对定位来构建一个下拉菜单,但在此之前,我们需要先看看它的搭档:相对定位。

相对定位

相对定位可能是最不被理解的定位类型。
当第一次给元素加上 position: relative 的时候,你通常看不到页面上有任何视觉改变。
相对定位的元素以及它周围的所有元素,都还保持着原来的位置(尽管你可能会看到某些元素跑到另一些元素前面,后面会解释这个问题)。

如果加上 top、right、bottom 和 left 属性,元素就会从原来的位置移走,但是不会改变它周围任何元素的位置。
如图7-4所示,四个 inline-block 元素,给第二个元素加上三个额外的属性:position: relative、top: 1em、left: 2em,将其从初始位置移走,但是其他元素没有受到影响。
它们还是围绕着被移走元素的初始位置,跟随着正常的文档流。


图7-4 使用相对定位将第二个元素移走

设置 top: 1em 将元素从原来的顶部边缘向下移动了 1em;
设置 left: 2em 将元素从它来的左侧边缘向右移动了2em。这可能导致元素跟它下面或者旁边的元素重叠。
在定位中,也可以使用负值,比如 bottom: -1em 也可以像 top: 1em 那样将元素向下移动 1em。

说明:
跟固定或者绝对定位不一样,不能用 top、right、bottomleft 改变相对定位元素的大小。
这些值只能让元素在上、下、左、右方向移动。可以用 top 或者 bottom,但它们不能一起用(bottom 会被忽略)。
同理,可以用 leftright,但它们也不能一起用(right 会被忽略)。

有时可以用这些属性调整相对元素的位置,把它挤到某个位置,但这只是相对定位的一个冷门用法。
更常见的用法是使用 position: relative给 它里面的绝对定位元素创建一个包含块

创建一个下拉菜单

接下来我们用相对和绝对定位创建一个下拉菜单。
它的初始状态是一个简单的矩形,当用户鼠标悬停到上面时,会弹出一个链接列表,如图7-5所示。


图7-5 下拉菜单

代码清单7-5是该菜单的标记。将它添加到HTML中,放在 <div class="modal"> 的结束标签 </div> 后面。
这段代码包含了一个容器元素,之后我们会将它的内容居中,并让它跟顶部条的内容对齐。
我还在弹出列表下面放了一个 <h1> 标签,以展示弹出列表如何出现在其他网页内容前面。

代码清单7-5 添加下拉菜单的HTML

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<div class="container">
    <nav>
      <div class="dropdown">
        <div class="dropdown-label">Main Menu</div>
        <div class="dropdown-menu">
          <ul class="submenu">
            <li><a href="/">Home</a></li>
            <li><a href="/coffees">Coffees</a></li>
            <li><a href="/brewers">Brewers</a></li>
            <li><a href="/specials">Specials</a></li>
            <li><a href="/about">About us</a></li>
          </ul>
        </div>
      </div>
    </nav>

    <h1>Wombat Coffee Roasters</h1>
  </div>

下拉菜单容器包含两个子元素:一个始终显示的灰色矩形标签以及一个下拉菜单。
下拉菜单用显示和隐藏表示菜单展开和收起。因为它会是绝对定位的,所以当下拉菜单显示时不会改变网页的布局,这意味着它显示时会出现在其他内容前面。

接下来,给下拉菜单容器加上相对定位。
这样会给绝对定位的菜单创建一个包含块。将代码清单7-6添加到样式表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
.container {
    width: 80%;
    max-width: 1000px;
    margin: 1em auto
}

.dropdown {
    display: inline-block;
    /* 创建包含块 */
    position: relative;
}

.dropdown-label {
    padding: 0.5em 1.5em;
    border: 1px solid #ccc;
    background-color: #eee;
}

.dropdown-menu {
    /* 最初隐藏菜单 */
    display: none;
    position: absolute;
    /* 将菜单移动到下拉菜单下面 */
    left: 0;
    top: 2.1em;
    min-width: 100%;
    background-color: #eee;
}
/* 鼠标悬停时显示菜单 */
.dropdown:hover .dropdown-menu {
    display: block;
}

.submenu {
    padding-left: 0;
    margin: 0;
    list-style-type: none;
    border: 1px solid #999;
}

.submenu > li + li {
    border-top: 1px solid #999;
}

.submenu > li > a {
    display: block;
    padding: 0.5em 1.5em;
    background-color: #eee;
    color: #369;
    text-decoration: none;
}

.submenu > li > a:hover {
    background-color: #fff;
}

当移动鼠标指针到主菜单标签时,下拉菜单就会从下面弹出。
请注意,这里是在整个容器上设置 :hover 状态来打开菜单。
也就是说只要鼠标停在它的任何内容上,无论是 dropdown-label 还是 dropdown-menu,菜单都会保持打开状态。

绝对定位的 dropdown-menu 设置了 left: 0,让其左边和整个容器的左侧对齐。
然后它使用 top: 2.1em 将其顶部边缘放在标签下面(算上内边距和边框,标签高 2.1em)。
min-width 为 100%,保证它至少等于容器的宽度(容器宽度由 dropdown-label 决定)。
之后用 submenu 类给下来菜单内的菜单加上样式。
(如果现在打开模态框,你就会发现它以一种奇怪的方式位于下拉菜单后面。没关系,我们很快就会解决这个问题。)

使用下拉菜单的几个重点

简单起见,代码清单7-6的例子在用户鼠标悬停的时候用了一个 :hover 伪类打开菜单。这个例子不完整。
通常情况下,更稳健的方式是使用 JavaScript 添加和移除一个控制菜单开关的类名。这样就能在打开和关闭菜单之前添加适当的延迟,防止用户在鼠标快速滑过时无意间触发 :hover

另外,虽然这个例子用鼠标能正常生效,但在触屏设备上会无效(只有一部分触屏设备会在轻触的时候触发 :hover 状态)。
该例子也没有解决用屏幕阅读器或者用键盘切换时的可访问性问题。
更严谨的做法是增强下拉菜单的功能,确保能用触屏控制,并且当用户使用Tab键切换菜单项的时候保持菜单打开。

实现这些功能的 JavaScript 代码不在本书的讨论范围内,但是如果你很擅长 JavaScript,就可以写代码解决以上问题。
你也可以借助实现了下拉功能的第三方库,然后用CSS来定制菜单的样式。

创建一个 CSS 三角形

下拉菜单距离完美还差一步。现在它已能正常工作,但用户无法一眼察觉到主菜单标签下面还有更多内容。
我们来给标签加上一个小的向下箭头,告诉用户还有更多内容。

我们可以用边框画一个三角形当作向下箭头。
这里用标签的 ::after 伪元素来画三角形,然后使用绝对定位将它放到标签的右边。

大多数情况下,我们会给一个元素加上较细的边框,通常 1px 或者 2px 就够了,但如果把边框变得像图7-6那样粗呢?
图中给每条边都加了独特的颜色,用来标出每条边的起始位置。


图7-6 带粗边框的元素

注意观察角上两条边的边缘接触的地方:它们形成了一个对角边。
再观察一下将元素的宽和高缩小到0时会发生什么(如图7-7所示)。所有的边都汇聚到一起最后在中间连接起来了。


图7-7 元素没有宽和高时,每条边都变成了一个三角形

元素四周的边都变成了三角形。顶部的边箭头指向下边,右边的边指向左边,以此类推。
基于这个现象,可以用一条边作为三角形,然后将剩下的边设置为透明。
元素的左右边都透明,而顶部边可见,就会如图7-8所示,形成一个简单的三角形。


图7-8 用元素的上边框做的三角形

我们给 dropdown-label::after 伪元素加上样式,做一个三角形,并让它绝对定位。
将代码清单7-7添加到样式表。

代码清单7-7 在下拉标签里绝对定位一个三角形

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
.dropdown-label {
    /* 增加右侧边距,给箭头留出空间 */
    padding: 0.5em 2em 0.5em 1.5em;
    border: 1px solid #ccc;
    background-color: #eee;
}

.dropdown-label::after {
    content: "";
    /* 在标签的右边定位元素 */
    position: absolute;
    right: 1em;
    top: 1em;
    /* 用上边框做一个向下的箭头 */
    border: 0.3em solid;
    border-color: black transparent transparent;
}
.dropdown:hover .dropdown-label::after {
    top: 0.7em;
    /* 鼠标悬停时,让箭头向上 */
    border-color: transparent transparent black;
}

伪元素因为没有内容,所以没有宽或高。
然后用 border-color 简写属性设置上边框为黑色,左右和下面的边框为透明,构造一个向下的箭头。
dropdown-label 右边用内边距留出了空间,用来放三角形。最后的效果如图7-9所示。


图7-9 带向下箭头的下拉菜单标签

打开菜单,箭头方向反转,朝向上面,表示菜单可以被关闭。
微调top值(从1em到0.7em),让向上的箭头看起来跟向下的箭头处于相同的位置。

另外你也可以用一个图片或者背景图来实现箭头,但是用短短几行CSS代码就可以为用户免去不必要的网络请求。
加上这个小小的箭头,能给网站或应用程序增色不少。

这项技术还可以用来构建其他复杂形状,比如梯形、六边形和星形。
查看用CSS构建的各种形状,可以访问 css-tricks 网站上的文章 The Shapes of CSS。

层叠上下文和 z-index

定位非常有用,但也需要弄清楚它会带来什么后果。
当把一个元素从文档流中移除时,我们就需要管理之前由文档流处理的所有事情了。

首先要确保元素不会不小心跑到浏览器视口之外,导致用户会看不到元素。其次要保证元素不会不小心挡住重要内容。

最后还有层叠的问题。在同一页面定位多个元素时,可能会遇到两个不同定位的元素重叠的现象。
有时我们会发现“错误”的元素出现在其他元素之前。
事实上,我们已经故意设置了这样的场景来演示这个问题。

在前面构建的网页里,通过点击网页头部的 Sign up 按钮打开模态框。
如果在HTML里下拉菜单的标记位于模态框之后,页面看起来就如图7-10所示。注意下拉菜单现在出现在模态框前面。


图7-10 模态框错误地出现在下拉菜单下面

有很多方法可以解决这个问题。
在此之前,要了解浏览器如何决定元素层叠顺序,首先需要知道浏览器是如何渲染页面的。

理解渲染过程和层叠顺序

浏览器将 HTML 解析为 DOM 的同时还创建了另一个树形结构,叫作渲染树(render tree)。
它代表了每个元素的视觉样式和位置。同时还决定浏览器绘制元素的顺序。
顺序很重要,因为如果元素刚好重叠,后绘制的元素就会出现在先绘制的元素前面。

通常情况下(使用定位之前),元素在 HTML 里出现的顺序决定了绘制的顺序。考虑以下代码里的三个元素:

1
2
3
<div>one</div>
<div>two</div>
<div>three</div>

它们的层叠行为如图 7-11 所示。这里使用了负的外边距让元素重叠,但并未使用任何定位。
后出现在标记里的元素会绘制在先出现的元素前面。


图7-11 三个元素正常层叠:HTML 里面后出现的元素在先出现的元素前面

定位元素时,这种行为会改变。浏览器会先绘制所有非定位的元素,然后绘制定位元素。
默认情况下,所有的定位元素会出现在非定位元素前面。
如图7-12所示,给前两个元素加了 position: relative,它们就绘制到了前面,覆盖了静态定位的第三个元素,尽管元素在HTML里的顺序并未改变。


图7-12 定位元素绘制在静态元素之前

注意,在定位元素里,第二个定位元素还是出现在第一个定位元素前面。
定位元素会被放到前面,但是基于源码的层叠关系并没有改变。

也就是说在上述网页里,模态框和下拉菜单都会出现在静态内容之前(符合预期),但是源码里后出现的元素会绘制在先出现的元素之前。
解决这个问题的一个办法是在源码里将 <div class="modal"> 及其内容移到下拉菜单后面。

通常情况下,模态框要放在网页内容的最后,</body> 关闭标签之前。
大多数构建模态框的 JavaScript 库会自动这样做。
因为模态框使用固定定位,所以不必关心它的标记出现在哪里,它会一直定位到屏幕中间。

改变固定定位元素的标记位置不会产生不好的影响,但是对相对定位或绝对定位的元素来说,通常无法用改变标记位置的方法解决层叠问题。
相对定位依赖于文档流,绝对定位元素依赖于它的定位祖先节点。
这时候需要用 z-index 属性来控制它们的层叠行为。

用 z-index 控制层叠顺序

z-index 属性的值可以是任意整数(正负都行)。z 表示的是笛卡儿 x-y-z 坐标系里的深度方向。
拥有较高 z-index 的元素出现在拥有较低 z-index 的元素前面。拥有负数z-index的元素出现在静态元素后面。

使用 z-index 是解决网页层叠问题的第二个方法。该方法不要求修改 HTML 的结构。
modal-backdrop 的 z-index 设置为 1,将 modal-body的 z-index 设置为 2(确保模态框的主体在蒙层前面)。按照代码清单7-8更新样式表。

代码清单7-8 给模态框加上 z-index,使其出现在下拉菜单之前

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.modal-backdrop {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.5);
    /* 将模态框的蒙层拉到没有设置 z-index 的元素前面 */
    z-index: 1;
}

.modal-body {
    position: fixed;
    top: 3em;
    bottom: 3em;
    right: 20%;
    left: 20%;
    padding: 2em 3em;
    background-color: white;
    overflow: auto;
    /* 将模态框猪蹄拉到蒙层前面 */
    z-index: 2;
}

z-index 的行为很好理解,但是使用它时要注意两个小陷阱。
第一,z-index 只在定位元素上生效,不能用它控制静态元素。第二,给一个定位元素加上 z-index 可以创建层叠上下文。

理解层叠上下文

一个层叠上下文包含一个元素或者由浏览器一起绘制的一组元素。
其中一个元素会作为层叠上下文的根,比如给一个定位元素加上 z-index 的时候,它就变成了一个新的层叠上下文的根。
所有后代元素就是这个层叠上下文的一部分。

不要将层叠上下文跟 BFC 弄混了,它们是两个独立的概念,尽管不一定互斥。
层叠上下文负责决定哪些元素出现在另一些元素前面,而BFC负责处理文档流,以及元素是否会重叠。

实际上将层叠上下文里的所有元素一起绘制会造成严重的后果:层叠上下文之外的元素无法叠放在层叠上下文内的两个元素之间。
换句话说,如果一个元素叠放在一个层叠上下文前面,那么层叠上下文里没有元素可以被拉到该元素前面。
同理,如果一个元素被放在层叠上下文后面,层叠上下文里没有元素能出现在该元素后面。

这么说比较绕,下面用一个例子来演示。新建一个HTML页面,添加代码清单7-9里的标记。

代码清单7-9 层叠上下文的例子

1
2
3
4
5
6
<div class="box one positioned">
    one
    <div class="absolute">nested</div>
  </div>
  <div class="box two positioned">two</div>
  <div class="box three">three</div>

这段代码包含了三个盒子,其中两个被定位,并且 z-index 为 1,第一个盒子里面有一个绝对定位的元素,它的 z-index 为 100。
虽然第一个盒子的 z-index 很高,但还是出现在第二个盒子后面,因为它的父元素,即第一个盒子形成的层叠上下文在第二个盒子后面(如图7-13所示)。


图7-13 整个层叠上下文相对于页面上其他元素叠放

代码清单7-10是这个场景的样式,将其添加到网页里。这里面大部分代码是给盒子添加大小和颜色,以便看清层叠顺序。
另外用负的外边距让元素重叠。而最核心的代码是给每个元素加上 position 和 z-index。

代码清单7-10 创建层叠上下文

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
body {
    margin: 40px;
}

.box {
    display: inline-block;
    height: 200px;
    width: 200px;
    line-height: 200px;
    text-align: center;
    border: 2px solid black;
    background-color: #ea5;
    margin-left: -60px;
    vertical-align: top;
}

.one {
    margin-left: 0;
}

.two {
    margin-top: 30px;
}

.three {
    margin-top: 60px;
}

/* 每个定位的盒子都创建了一个层叠上下文, z-index 为 1 */
.positioned {
    position: relative;
    background-color: #5ae;
    z-index: 1;
}

.absolute {
    position: absolute;
    top: 1em;
    right: 1em;
    height: 2em;
    background-color: #fff;
    border: 2px dashed #888;
    /* z-index 只控制元素在它所处层叠上下文内的层叠顺序 */
    z-index: 100;
    line-height: initial;
    padding: 1em;
}

叠放在第二个盒子后面的第一个盒子是一个层叠上下文的根。
因此,虽然它的 z-index 值很高,但是它内部的绝对定位元素不会跑到第二个盒子前面。
在浏览器开发者工具里试验一下,感受这种关系,改变每个元素的z-index看看会发生什么。

说明:
给一个定位元素加上 z-index 是创建层叠上下文最主要的方式,但还有别的属性也能创建,比如小于 1 的 opacity 属性,还有 transform、filter 属性。
由于这些属性主要会影响元素及其子元素渲染的方式,因此一起绘制父子元素。文档根节点(<html>)也会给整个页面创建一个顶级的层叠上下文。

所有层叠上下文内的元素会按照以下顺序,从后到前叠放:

  • 层叠上下文的根
  • z-index 为负的定位元素(及其子元素)
  • 非定位元素
  • z-index 为 auto 的定位元素(及其子元素)
  • z-index 为正的定位元素(及其子元素)

用变量记录z-index:
如果不根据组件的优先级定义清晰的层叠顺序,那么一个样式表很容易演变成一场z-index大战。
如果没有清晰的说明,开发人员在给一个模态框之类的元素添加样式时,为了不被其他元素遮挡,就会设置一个高得离谱的z-index,比如999999。
这样的事情重复几次后,大家就只能凭感觉给一个新的组件设置z-index。

如果你使用预处理器,比如LESS或SASS,或者你支持的所有浏览器都支持自定义属性,就能很方便地处理这个问题。
将所有的z-index都定义为变量放到同一个地方,如下代码片段所示。这样就能清晰地看到哪些元素在前哪些元素在后。

1
2
3
4
5
--z-loading-indicator: 100;
--z-nav-menu:             200;
--z-dropdown-menu:      300;
--z-modal-backdrop:     400;
--z-modal-body:          410;

将增量设为10或者100,这样就能在需要的时候往中间插入新值。

如果发现 z-index 没有按照预期表现,就在DOM树里往上找到元素的祖先节点,直到发现层叠上下文的根。然后给它设置z-idnex,将整个层叠上下文向前或者向后放。
还要注意多个层叠上下文嵌套的情况。

网页很复杂时,很难判断是哪个层叠上下文导致的问题。
因此,在创建层叠上下文的时候就一定要多加小心,没有特殊理由的话不要随意创建,尤其是当一个元素包含了网页很大一部分内容的时候。
尽可能将独立的定位元素(比如模态框)放到DOM的顶层,结束标签 </body> 之前,这样就没有外部的层叠上下文能束缚它们了。

有些开发人员会忍不住给页面的大量元素使用定位。一定要克制这种冲动。
定位用得越多,网页就越复杂,也就越难调试。
如果你定位了大量元素,就回头评估一下现在的情况,尤其是当你发现很难调试出自己想要的布局时,一定要反思。
如果可以用别的方法实现某个布局,应该优先用那些方法。

如果能够依靠文档流,而不是靠明确指定定位的方式实现布局,那么浏览器会帮我们处理好很多边缘情况。
记住,定位会将元素拉出文档流。一般来说,只有在需要将元素叠放到别的元素之前时,才应该用定位。

粘性定位

人们已经用四种主要的定位类型(静态、固定、绝对以及相对)很长时间了,不过现在浏览器还提供了一种新的定位类型:粘性定位(sticky positioning)。
它是相对定位和固定定位的结合体:正常情况下,元素会随着页面滚动,当到达屏幕的特定位置时,如果用户继续滚动,它就会“锁定”在这个位置。
最常见的用例是侧边栏导航。

曾经只有 Firefox 浏览器支持粘性定位,现在 Chrome 和 Edge 浏览器都支持了该特性。
Safari则需要加上浏览器前缀(position: -webkit-sticky)才支持。
记得在Can I Use网站中检索 CSS position:sticky 查看最新的支持情况。
如果不支持,通常要用固定定位或者绝对定位来回退处理。

你的网页已经有了模态框和下拉菜单,现在升级一下网页,将其改成两栏布局,右边栏是一个粘性定位的侧边栏,效果如图7-14所示。


图7-14 粘性定位的侧边栏在初始状态下位置正常

网页刚加载的时候,侧边栏的位置一切正常。
网页滚动,它也跟着滚动直到滚到快要离开视口的时候,它会锁定在那个位置。
当网页的剩余部分继续滚动时,它却好像固定定位的元素一样停留在屏幕上,效果如图7-15所示。


图7-15 侧边栏固定在一个位置

接下来修改网页结构,定义两栏。在HTML里将容器改成如代码清单7-11所示的代码。
把之前的内容(下拉菜单和网页标题)放在左边栏,再添加一个右边栏放“affix”菜单。

代码清单7-11 将网页改为带侧边栏的两栏布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<div class="container">
  <main class="col-main">
    <nav>
      <div class="dropdown">
        <div class="dropdown-label">Main Menu</div>
        <div class="dropdown-menu">
          <ul class="submenu">
            <li><a href="/">Home</a></li>
            <li><a href="/coffees">Coffees</a></li>
            <li><a href="/brewers">Brewers</a></li>
            <li><a href="/specials">Specials</a></li>
            <li><a href="/about">About us</a></li>
          </ul>
        </div>
      </div>
    </nav>
    <h1>Wombat Coffee Roasters</h1>
  </main>

  <aside class="col-sidebar">
    <div class="affix">
      <ul class="submenu">
        <li><a href="/">Home</a></li>
        <li><a href="/coffees">Coffees</a></li>
        <li><a href="/brewers">Brewers</a></li>
        <li><a href="/specials">Specials</a></li>
        <li><a href="/about">About us</a></li>
      </ul>
    </div>
  </aside>
</div>

接下来更新CSS,将容器设为弹性容器,设置两栏的宽度。
本例复用了下拉菜单的子菜单的样式,当然你也可以给侧边栏添加其他的元素和样式。将代码清单7-12加到样式表里。

代码清单7-12 创建一个两栏布局以及粘性定位的侧边栏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.container {
    /* 将容器设置为弹性容器,实现两栏布局 */
    display: flex;
    width: 80%;
    max-width: 1000px;
    margin: 1em auto;
    /* 特意给容器设置高度 */
    min-height: 100vh;
}

/* 给两栏布局 */
.col-main {
    flex: 1 80%;
}

.col-sidebar {
    flex: 20%;
}

/* 给侧边栏的菜单添加粘滞定位,它会停在距离视口顶部 1em 的位置 */
.affix {
    position: sticky;
    top: 1em;
}

以上代码主要用来设置两栏布局。最后只用了两句声明来给affix元素定位。
top值设置了元素最终固定的位置:距离视口的顶部1em。

因为粘性元素永远不会超出父元素的范围,所以本例中affix不会超出col-sidebar的范围。
当滚动页面的时候,col-sidebar会一直正常滚动,但是affix会在滚动到特定位置时停下来。
如果继续滚动得足够远,粘性元素还会恢复滚动。这种情况只在父元素的底边到达粘性元素的底边时发生。
注意,只有当父元素的高度大于粘性元素时才会让粘性元素固定,因此这里我特意给弹性容器加上min-height,以便让父元素足够高。

总结

  • 模态框使用固定定位。
  • 下拉菜单、工具提示及其他动态交互使用绝对定位。
  • 实现这些功能时还要考虑可访问性。
  • 关于 z-index 有两个地方要注意:它只对定位元素有效;它会创建一个层叠上下文。
  • 在一个页面创建多个层叠上下文时一定要当心潜在的陷阱。
  • 使用粘性定位时注意浏览器的兼容性。

其他

相对定位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<head>
    <style type="text/css">
        h2.pos_left {
            position: relative;
            left: -20px;
        }
        h2.pos_right {
            position: relative;
            left: 20px;
        }
    </style>
</head>
<body>
    <h2>这是位于正常位置的标题</h2>
    <h2 class="pos_left">这个标题相对于其正常位置向左移动</h2>
    <h2 class="pos_right">这个标题相对于其正常位置向右移动</h2>
    <p>相对定位会按照元素的原始位置对该元素进行移动。</p>
    <p>样式 "left:-20px" 从元素的原始左侧位置减去 20 像素。</p>
    <p>样式 "left:20px" 向元素的原始左侧位置增加 20 像素。</p>
</body>

相对定位

绝对定位

生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。

元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<html>
<head>
<style type="text/css">
h2.pos_abs
{
position:absolute;
left:100px;
top:150px
}
</style>
</head>
<body>
<h2 class="pos_abs">这是带有绝对定位的标题</h2>
<p>通过绝对定位,元素可以放置到页面上的任何位置。下面的标题距离页面左侧 100px,距离页面顶部 150px。</p>
</body>
</html>

绝对定位

1
2
3
4
<div class="container">
    <div class="div1">div1</div>
</div>
<div>div2</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
div {
    width: 200px;
    height: 300px;
    background: yellow;
    border: 1px solid black;
    /* 定位 固定定位 */
    /* top bottom left right */
    /* position: fixed;
    bottom: 100px;
    right: 50px; */

    /* 相对定位: */
    /* position: relat ive; */
}
.container {
    margin: auto;
    background: green;
    position: relative;
}
.div1 {
    position: absolute;
    top: 50px;
    left: 100px;
    width: 100px;
    height: 100px;
}
/* .div1 {
    position: relative;
    top: 50px;
    left: 100px;
} */