CSS 当中的 position

CSS 当中的 position

我们在之前的章节当中介绍过了 CSS 当中的 float 和 display 属性,今天我们接着上回继续来看 CSS 当中另一个比较重要的属性,那就是 position,关于 position 的简单用法可以参考下面这个示例(转自 MDN)

基本语法

在 CSS 中,每一个元素都由一个矩形盒子所包含,每一个盒子都会具有一个内容区,内容区被一个内边距所包裹,内边距外是盒子的边框,并且在边框之外会有一个外边距用于与其他盒子分隔开来,这些你可以从下面这张图片看到

定位模式规定了一个盒子在总体的布局上应该处于什么位置以及对周围的盒子会有什么影响,定位模式包括了常规文档流,浮动,和几种类型的 position 定位的元素,它的属性可以取 absolute/relative/fixed/static/sticky 其中的一种,它们之间的区别如下

  • static,该关键字指定元素使用正常的布局行为,如果没有设置 postion,所以元素的 position 都是默认的 static,即元素在文档常规流中当前的布局位置,此时 top/right/bottom/left/z-index 属性均无效
  • relative,该关键字下,此时元素仍然处于正常流,且不改变 display 属性,元素会先放置在未添加定位时的位置,在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白),对 table-*-group/table-row/table-column/table-cell/table-caption 元素无效,可以理解为仍然占据原来空间,所以不影响其他元素布局,但是可能会覆盖别的元素
  • absolute,元素会被移出正常文档流,并不为元素预留空间,通过指定元素相对于最近的非 static 定位祖先元素的偏移,来确定元素位置,绝对定位的元素可以设置外边距(margins),且不会与其他边距合并
  • fixed,元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置,元素的位置在屏幕滚动时不会改变,fixed 属性会创建新的层叠上下文,当元素祖先的 transform/perspective/filter 属性非 none 时,容器由视口改为该祖先
  • sticky,元素根据正常文档流进行定位,然后相对它的最近滚动祖先(nearest scrolling ancestor)和 Containing Block(最近块级祖先,nearest block-level ancestor),包括 table-related 元素,基于 top/right/bottom/left 的值进行偏移,偏移值不会影响任何其他元素的位置,该值总是创建一个新的层叠上下文(stacking context)
    • 需要注意的是,一个 sticky 元素会固定在离它最近的一个拥有滚动机制的祖先上(当该祖先的 overflow 是 hidden/scroll/auto/overlay 时),即便这个祖先不是最近的真实可滚动祖先,这有效地抑制了任何 sticky

大多数情况下,height 和 width 被设定为 auto 的绝对定位元素,按其内容大小调整尺寸,但是被绝对定位的元素可以通过指定 top 和 bottom ,保留 height 未指定(即 auto),来填充可用的垂直空间,它们同样可以通过指定 left 和 right 并将 width 指定为 auto 来填充可用的水平空间,不过这里有一个需要注意的地方,就是如果 top 和 bottom 都被指定(严格来说,这里指定的值不能为 auto )的时候 top 优先

在深入展开之前,我们先来看看包含块(Containing Block)的和偏移属性的概念

包含块

包含块简单理解就是一个定位参考块,就是大盒子里套小盒子中那个大盒子,元素有 positon 属性就必然涉及到包含块,包含块简单来说分为两部分,即根元素与非根元素,根元素 HTML 的包含块(也称为初始包含块,Initial containing block,即根元素的包含框)是一个视窗大小的矩形,即 HTML 的父级 Document,而非根元素分为两种情况(positon: fixed 元素的包含块是由 viewport 决定的,和根元素无关)

  • 如果 position 值是 relative 或 static,包含块由最近的块级框、表单元格或行内祖先框的内容边界构成,也就是元素包含块为最近的块级(block/list-item/table)父元素的内容框 content-box
  • 如果 position 值是 absolute,包含块设置为最近的 position 值不是 static 的祖先元素(可以是任何类型),过程如下
    1. 如果这个祖先是块级元素,包含块则设置为该元素的内边距边界,换句话说就是由边框界定的区域
    2. 如果这个祖先是行内元素,包含块则设置为该祖先元素的内容边界
    3. 如果没有祖先,元素的包含块定义为初始包含块,即 Document

需要注意的是,由于元素可以定位到其包含块的外面,这与浮动元素使用负外边距浮动到其父元素内容区外面很类似,所以这里包含块实际上应该是定位上下文,或者定位父级

偏移属性

CSS 有三种基本的布局机制,普通流、浮动和绝对定位,利用定位可以准确地定义元素框相对于其正常位置应该出现的位置,或者相对于父元素、另一个元素甚至浏览器窗口本身的位置,但元素究竟如何定位,定位到什么位置,主要依靠偏移属性来决定

三种定位机制使用了四个属性来描述定位元素各边相对于其包含块的偏移,这四个属性被称为偏移属性,它们是 top/right/bottom/left,并且初始值都为 auto,可以应用于 position 值不是 static 的定位元素,如果使用的是百分数,则对于 top 和 bottom 而言它相对于包含块的 clientHeight,而对于 right 和 left 来说则相对于包含块的 clientWidth

这些属性描述了距离包含块最近边的偏移,top 描述了定位元素上外边界离其包含块的顶端有多远,如果 top 为正值,会把定位元素的上外边距边界下移,若为负值,则会把定位元素的上外边距移到其包含块的顶端之上,类似地,left 描述了定位元素的左外边距边界在其包含块左边界右边(正值)或左边(负值)有多远

如果是正值,会把定位元素的外边距边界移到包含块左边界右边,而负值则将其移到包含块左边界左边,所以,正值会导致向内偏移,使边界朝着包含块的中心移动,而负值会导致向外偏移,偏移定位元素的外边距边界时,带来的影响是元素的所有一切(包含外边距、边框、内边距和内容)都会在定位的过程中移动

定位元素的边界是指定位元素 margin 外侧的边界,包含块的包含区域是指包含块的 border 内侧的 padding + content 区域

在了解了包含块的和偏移属性的概念以后,我们就来看看各种不同的取值之间的差别,不过在本章当中,我们主要介绍 position 和 sticky 这两个属性,其他几个属性就简单带过

绝对定位

元素绝对定位时,会从文档流中完全删除,然后相对于其包含块定位,其边界根据偏移属性(top、left 等)放置,定位元素不会流入其他元素的内容,反之亦然,元素绝对定位时,会为其后代元素建立一个包含块,如果文档可滚动,绝对定位元素会随着它滚动,因为元素最终会相对于正常流的某一部分定位

一个绝对定位的元素的起点位置是相对于它的第一个 position 值不为 static 的父元素而言的,如果在它的父元素链上没有满足条件的父元素,那么绝对定位元素则会相对于文档窗口来进行定位,也就是说,当你在一个元素的样式上设置 position: absolute 则意味着需要考虑它的父元素,如果父元素的 position 值不为 static ,那么绝对定位元素的起点为父元素的左上角位置

如果父元素没有应用除了 static 以外的 position 定位,那么它会检查父元素的父元素是否有应用非 static 定位,如果该元素应用了定位,那么它的左上角便会成为绝对元素的起点位置,如果没有则会继续向上遍历 DOM 直到找到一个定位元素或者寻找失败以到达最外层的浏览器窗口,当元素绝对定位时,它的偏移属性表现如下

  • left: 0,元素的左边界(margin-left 外侧)位于包含块的左边界内侧(border-left 内侧)
  • top: 0,元素的上边界(margin-rop 外侧)位于包含块的上边界内侧(border-top 内侧)
  • right: 0,元素的右边界(margin-right 外侧)位于包含块的右边界内侧(border-right 内侧)
  • bottom: 0,元素的下边界(margin-bottom 外侧)位于包含块的下边界内侧(border-bottom 内侧)

当 top/right/bottom/left 四个值都为 auto 时(即都处于默认状态时)

  • left: auto,元素的左边界位于元素处于静态位置时的左边界
  • top: auto,元素的上边界位于元素处于静态位置时的上边界
  • right: auto,元素的右边界位于正好能包裹住元素的横向区域的右边界(margin-right 外侧)
  • bottom: auto,元素的下边界位于正好能包裹住元素的纵向区域的下边界(margin-bottom 外侧)

元素的静态位置是指元素在正常流中原本的位置,更确切的讲,顶端的静态位置是从包含块的上边界到假想框的上外边距边界之间的距离,假想框是假设元素 position 属性为 static 时元素的第一个框,如果这个假想框在包含块的上面,则这个值为负

格式化

对于普通流的元素来说,水平格式化的七大属性是 margin-left/border-left/padding-left/width/padding-right/border-right/margin-right,这些属性的值加在一起就是元素包含块的宽度,这往往也是块元素的父元素的 width 值(因为块级元素的父级元素几乎都是块级元素)垂直方向也类似,但是对于绝对定位元素则不相同,它的水平格式化等式为

1
left + margin-left + border-left-width + padding-left + width + padding-right + border-right-width + margin-right + right = Containing Block clientWidth

类似的,垂直格式化等式为

1
top + margin-top + border-top-width + padding-top + height + padding-bottom + border-bottom-width + margin-bottom + bottom = Containing Block clientHeight

也就是下图这样

auto

在之前偏移属性表现的时候,我们提到了 auto,其实 auto 值是用来弥补实际值与所需总和的差距,水平方向上,可以为 auto 的属性有 left/margin-left/width/margin-right/right,同样的,在垂直方向上,可以为 auto 的属性有 top/margin-top/height/margin-bottom/bottom

  • margin-left: auto; margin-right: auto; left: 0; right: 0;,块元素可以横向居中显示
  • margin-top: auto; margin-bottom: auto; top: 0; right: 0;,块元素可以纵向居中显示

所以我们可以结合上面两点来实现水平垂直居中显示

相对定位

相对定位的元素也是根据 top/right/bottom/left 四个属性来决定自己的位置的,但只是相对于它们原来所处于的位置进行移动,在某种意义上来说,为元素设置相对定位和为元素添加 margin 有点相似,但也有一个重要的区别,区别就是在围绕在相对定位元素附近的元素会忽略相对定位元素的移动

我们可以把它看做是一张图片的重像从真实的图片的位置开始进行了一点移动,它原始图片所占据的位置仍然保留,但我们已经没法再看到它,只能看到它的重像,这样就让元素之间可以进行位置的重叠,因为相对定位元素能够移动到其他元素所占据的空间中,如果相对定位元素离开了正常文档流,但仍然影响着围绕着它的元素,那些元素表现地就好像这个相对定位元素仍然在正常文档流当中

固定定位

固定定位的行为类似于绝对定位,但也有一些不同的地方,首先,固定定位总是相对于浏览器窗口来进行定位的,并且通过 top/right/bottom/left 属性来决定其位置,它抛弃了它的父元素,第二个不同点是固定定位的元素是固定的,它们并不随着页面的滚动而移动,你可以告诉元素它所处的位置并永远不再移动

在某种意义上说固定定位元素有点儿类似固定的背景图片,只不过它的外层容器块总是浏览器窗口罢了,如果你在 body 中设置一个背景图片那么它与一个固定定位的元素的行为时非常像的,只不过在位置上的精度会略少一些

这里提到固定定位和绝对定位的话,那就不得不提另外一个属性,那也就是 z-index 了,不过关于 z-index 的相关内容较多,所以我们会另起篇幅来进行介绍,这里可以先简单了解一下,参考下图

由上图可知,高的 z-index 位于低的 z-index 的上面并朝页面的上方运动,相反地,一个低的 z-index 在高的 z-index 的下面并朝页面下方运动,现在我们只需要记住这个维度的基本概念以及它的堆叠顺序,另外还要记住只有定位元素才能应用 z-index 属性

粘性定位

sticky 是 CSS 属性 position 中的一个可选值,跟我们用得比较多的 static/fixed/relative/absolute 一样,都是用来描述元素的定位方式,它是计算后位置属性为 sticky 的元素,简单的理解就是,在目标区域以内,它的行为就像 position: relative 一样,但是在滑动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时(比如 top:100px),这时 position: sticky 的效果就相当于 fixed 定位,固定到适当位置

其实可以说是相对定位 relative 和固定定位 fixed 的结合,元素固定的相对偏移是相对于离它最近的具有滚动框的祖先元素,如果祖先元素都不可以滚动,那么就会相对于 viewport 来计算元素的偏移量,比如下面这个示例

用代码实现的话,大概是下面这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="container">
<nav>我是导航栏</nav>
<div class="content">
<p>我是内容栏</p>
<p>我是内容栏</p>
<p>我是内容栏</p>
<p>我是内容栏</p>
<p>我是内容栏</p>
<p>我是内容栏</p>
<p>我是内容栏</p>
<p>我是内容栏</p>
</div>
</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
.container {
background: #eee;
width: 600px;
height: 1000px;
margin: 0 auto;
}

nav {
position: -webkit-sticky;
position: sticky;
top:0;

}

nav {
height: 50px;
background: #999;
color: #fff;
font-size: 30px;
line-height: 50px;
}

.content {
margin-top: 30px;
background: #ddd;
}

p {
line-height: 40px;
font-size: 20px;
}

其实 position:sticky 在使用上很简单,就两句核心代码

1
2
3
4
div {
position: sticky;
top: 0;
}

但是它的生效是有一定的限制的,总结如下

  • 须指定 top/right/bottom/left 四个阈值其中之一,才可使粘性定位生效,否则其行为与相对定位相同
    • 上面的 top: 0 的意思是当元素滑动到距离视口 0px 时再继续滑动,元素吸顶
    • 并且 top 和 bottom 同时设置时,top 生效的优先级高,left 和 right 同时设置时 left 的优先级高
  • 设定为 position: sticky 元素的任意父节点的 overflow 属性必须是 visible,否则 position: sticky 不会生效,这里需要解释一下:
    • 如果 position: sticky 元素的任意父节点定位设置为 overflow: hidden,则父容器无法进行滚动,所以 position: sticky 元素也不会有滚动然后固定的情况
    • 如果 position: sticky 元素的任意父节点定位设置为 position: relative/absolute/fixed,则元素相对父元素进行定位,而不会相对 viewprot 定位
  • 达到设定的阀值,也就是设定了 position: sticky 的元素表现为 relative 还是 fixed 是根据元素是否达到设定了的阈值决定的

下面再来看一个示例

1
2
3
4
5
6
<div class="container">
<div class="sticky-box">内容1</div>
<div class="sticky-box">内容2</div>
<div class="sticky-box">内容3</div>
<div class="sticky-box">内容4</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.container {
background: #eee;
width: 600px;
height: 1000px;
margin: 0 auto;
}

.sticky-box {
position: -webkit-sticky;
position: sticky;
height: 60px;
margin-bottom: 30px;
background: #ff7300;
top: 0px;
}

div {
font-size: 30px;
text-align: center;
color: #fff;
line-height: 60px;
}

我们在之前的部分曾经提到过,sticky 效果只在 Containing Block 内有效,当 Containing Block 滑出屏幕时,Stickey Element 也跟着滑走,但是针对于上面这个示例,多个 Sticky Element 放在一块就有了前一个被后一个顶出去的效果,实际上并不是真的被顶出去,而是 Containing Block 把它拖走了

其实使用 sticky 比较优势的地方是移动端,在没有 CSS Sticky 之前,类似的效果都是使用 JavaScript 来进行实现的,它一般的流程是下面这样

  • 监听滚动事件,计算目标元素距离视口的距离
  • 距离不满足条件时,按兵不动
  • 距离满足条件时,创建占位元素,修改目标元素定位方式为 fixed
1
2
3
4
window.addEventListener('scroll', () => {
const rect = elem.getBoundingClientRect()
// 计算目标元素和视口的距离
})

但是使用 CSS Sticky 以后,工作都交给 GPU 了,不再占用 JavaScript 主线程的资源,所以在移动端上的表现异常流畅

参考

# CSS

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×