我们在之前的章节当中介绍过了 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
的祖先元素(可以是任何类型),过程如下- 如果这个祖先是块级元素,包含块则设置为该元素的内边距边界,换句话说就是由边框界定的区域
- 如果这个祖先是行内元素,包含块则设置为该祖先元素的内容边界
- 如果没有祖先,元素的包含块定义为初始包含块,即
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 | <div class="container"> |
1 | .container { |
其实 position:sticky
在使用上很简单,就两句核心代码
1 | div { |
但是它的生效是有一定的限制的,总结如下
- 须指定
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 | <div class="container"> |
1 | .container { |
我们在之前的部分曾经提到过,sticky
效果只在 Containing Block
内有效,当 Containing Block
滑出屏幕时,Stickey Element
也跟着滑走,但是针对于上面这个示例,多个 Sticky Element
放在一块就有了前一个被后一个顶出去的效果,实际上并不是真的被顶出去,而是 Containing Block
把它拖走了
其实使用 sticky
比较优势的地方是移动端,在没有 CSS Sticky
之前,类似的效果都是使用 JavaScript
来进行实现的,它一般的流程是下面这样
- 监听滚动事件,计算目标元素距离视口的距离
- 距离不满足条件时,按兵不动
- 距离满足条件时,创建占位元素,修改目标元素定位方式为
fixed
1 | window.addEventListener('scroll', () => { |
但是使用 CSS Sticky
以后,工作都交给 GPU
了,不再占用 JavaScript
主线程的资源,所以在移动端上的表现异常流畅