你真的了解 sticky 吗?

2,945 阅读6分钟

问题

最近新起一个项目,直接上最新版本的 antd pro ,但是此框架封装太死,页面的框架不适合现有项目的结构(但是项目的代码结构,以及其他工具的集成还是很好用的 ),所以重写页面框架。框架很简单,其实就是上中下结构,“上”部的 header 组件需要吸顶,在吸顶的实现过程中遇到以下问题:

  • 将 header 设置为 position: fixed; width:100%
  • 设置 header 外层组件 min-height:1200px, 当页面宽度收缩至 1200px 时不再收缩,出现滚动条。拖动滚动条时,header 中被浏览器隐藏掉的 头像,不随横向滚动条的移动而移动。
  • 由于业务中 header 的存在可能内容较多的情况,而交互设计中没有类似于移动端的 @media ,不能适当隐藏某些组件以达到布局上的效果。

所以需要使用另外的方案来解决,达到 header 纵向不随页面滚动;横向随着页面滚动的效果。

知识储备

上述页面中使用到了 position: fixed; ,记忆中的 position 似乎不是特别清晰,让我们来回顾一下。只有巩固根基才能开支散叶,形成知识网络。

查看 MDN 的解释,如下:

static:

该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 top, right, bottom, left 和 z-index 属性无效。

简单地说就是所有元素的 position 默认值,浏览器默认行为。

relative:相对定位

该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。position:relative 对 table-*-group, table-row, table-column, table-cell, table-caption 元素无效。

图中蓝色盒子 Two ,首先定位在属于原本文档流的位置,相对于此位置进行上和左的偏移。

#two {
  position: relative;
  top: 20px;
  left: 20px;
  background: blue;
}

absolute: 绝对定位

相对定位的元素并未脱离文档流,而绝对定位的元素则脱离了文档流。在布置文档流中其它元素时,绝对定位元素不占据空间。绝对定位元素相对于最近的非 static 祖先元素定位。当这样的祖先元素不存在时,则相对于ICB(inital container block, 初始包含块)

上段描述有两个重点:

  • 脱离文档流
  • 定位于最近的非 static 的元素, 如果没有那就是初始包含块,即 <html> 标签.
文档流是什么呢

可以浅显地理解为,所有页面元素有既定的默认排版顺序,依次排版,有条不紊。

脱离文档流,可以理解为在不在原本的排版规则之内,在原本的文档流切面上添加一个属于的自己的切面。不占任何原本文档流的任何位置。

fixed: 固定定位

固定定位与绝对定位相似,但元素的包含块为 viewport 视口。该定位方式常用于创建在滚动屏幕时仍固定在相同位置的元素。

固定定位的定位方式相较于绝对定位发生了改变,基于视口定位,也可以抽象为就是浏览器的可视区域,我们之前的 header 也就是 fixed 的定位。 同样 fixed 也脱离于文档流。

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 ”行为。

这是一个 relativefixed 结合体,解读一下上面官方的解释:

  • 触发 relative 和 fixed 的切换需要条件。
  • 须指定 top, right, bottom 或 left 四个中的至少一个作为切换条件。
  • 粘性元素依托于父级的滚动,而且该父级 overflow 不能是 hidden, scroll, auto, 或 overlay 。
  • 在文档流之内

解决方案

sticky 实际上有更加细致地控制。我们修改之前的代码


/*before*/
.header {
  display: flex;
  justify-content: space-between;
  background: #fff;
  box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.12);
  align-items: center;
  position: fixed;
  z-index: 999;
  top: 0;
  width: 100%;
}

/*after*/
.header {
  display: flex;
  justify-content: space-between;
  background: #fff;
  box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.12);
  align-items: center;
  position: sticky;
  z-index: 999;
  top: 0;
}


将 position 改为 sticky 后马上达到了效果,可是为什么呢?

我们将触发条件提炼出来:

  • position: sticky
  • 边界: top: 0 ,使用 top 当做触发条件。
  • 当父级滚动的时候, header 离 viewport 上端超过了 0px ,header 由 raletive 变成了 fixed。
  • 由于只设置在 top 方向的边界,所以在纵向滚动时没有边界,故纵向滚动时仍为 reletive,跟随页面的滚动而滚动

我们来设置一下纵向的 sticky 触发条件

/*before*/
.header {
  display: flex;
  justify-content: space-between;
  background: #fff;
  box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.12);
  align-items: center;
  position: sticky;
  z-index: 999;
  top: 0;
}

/*after*/
.header {
  display: flex;
  justify-content: space-between;
  background: #fff;
  box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.12);
  align-items: center;
  position: sticky;
  z-index: 999;
  top: 0;
  right:10px;
}

但是横向滑动并没有生效。使 sticky 生效还需要一个条件,该元素必须存在对应触发条件上的宽或高

让我们再设置一下 width :

.header {
  display: flex;
  justify-content: space-between;
  background: #fff;
  box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.12);
  align-items: center;
  position: sticky;
  z-index: 999;
  top: 0;
  left:0;
  width:800px;
}

此时 sticky 元素的行为已经和 fixed 表现一致,这也印证了 sticky 可以更细粒度的控制。

知识联想

文档流

上述文字中提到了文档流,那到底什么是文档流呢 ? 关键字在于

官方的描述为 Normal flow,这里我们称为 文档流

首先需要区分 html 的元素(标签)类型:块级元素、行内元素、行内-块级元素。(不展开总结)

根据标签类型可以在文档流中依次排开,自上而下、自左至右。

position 和 display 的区别

position: 顾名思义可控制元素的位置。 而 display 分为两种类型:

  • 外部显示类型定义了元素怎样参与流式布局的处理。
    • 比如改变div 为 行内-块级元素。
  • 内部显示类型定义了元素内子元素的布局方式。
    • 比如 flex、grid ,控制元素内部的布局

display 的外部显示行为,都在文档流之中,position 可以使元素脱离于文档流

这样就清晰明了了。