fixed布局踩坑引发的深思

2,017 阅读4分钟

fixed布局踩坑引发的深思

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

故事背景

scene 1:前几天看到同事在处理一个页面 bug,大致是有一个输入组件需要点击后置顶,ta 设计成点击后应用 fixed 布局,设置了 top = 0,left = 0,但是看起来组件并没有贴到最顶层,查看页面元素,其中该组件的祖先元素设置了 transform 属性。

scene 2:当天晚上,一时兴起,我写了一个遮罩+弹窗提示的功能,遮罩和弹窗用到了fixed布局,其中遮罩实现了背景模糊的功能。本以为能让弹窗居中,却没有达到预期的效果,弹窗贴到了屏幕底部。

知识点

fixed 定位: 元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。fixed 属性会创建新的层叠上下文。当元素祖先的 transformperspective 或 filter 属性非 none 时,容器由视口改为该祖先。 ——源自《position - CSS(层叠样式表) | MDN (mozilla.org)

作为 css 菜鸟的我,并不知道其实 fixed 定位不是一定就相对于屏幕视口定位的,而是在某几种 case 下,会相对于某些祖先元素进行定位,如上文 MDN 文档所说的。我一开始还在想会不会跟父容器也设置了 fixed 定位有关系,ε=(´ο`*)))唉。

好巧不巧,那个同事给祖先元素利用transform进行了一个平移操作,而我给弹窗的父组件,也就是遮罩,设置了一个背景模糊的效果——通过 backdrop-filter: blur(4px)实现(mdn可没提这个啊,filter包括了backdrop-filter ?),都刚好触及了fixed定位的特例,因此导致没有相对视口定位。

bug 复现与改进

核心代码大致如下

const Modal = (/*...*/) => {
  // ...
  const styleModal: CSSProperties = {
    boxShadow: '0 2px 10px var(--shadow-color)',
    minWidth: '200px',
    maxWidth: '300px',
    minHeight: '150px',
    position: 'fixed',
    top: '50%',
    left: '50%',
    zIndex: 101,
    padding: '10px',
    transform: 'translate(-50%, -50%)',
    color: 'var(--text-color)',
    background: 'var(--bg-color)',
    wordBreak: 'break-all'
  }
  const styleMask: CSSProperties = {
    width: '100vw',
    height: '100vw',
    top: 0,
    left: 0,
    position: 'fixed',
    backdropFilter: 'blur(10px)',
    zIndex: 100,
    background: '#33333333'
  }
  // ...
  return (
    <div style={styleMask}>
      <div className='' style={styleModal}>
       <!-- ... -->
      </div>
    </div>
  )
}
export default Modal

原本我以为是这样的

image.png 结果是这样的

image.png

原因在上一节已经说了,那么怎样实现我想要的,弹窗居中的效果呢?

方案一:将里面的弹窗元素的 position 改为 sticky sticky会相对于最近可滚动祖先定位,只要没有特别设置祖父容器的overflow,就是相对根元素定位,达到和fixed定位相同的效果。

方案二:将fixed布局元素放在最顶层标签 只要没有存在特殊的父元素,就不会受父容器的影响,简单直接,对于多个fixed布局的元素还可以设置z-index控制彼此的层级关系。

后记

11月1日回到家已经是10点多了,本想在12点前发出在掘金的第一篇文章的,结果临近12点都没写到干货,想匆匆提交,却连标题都没找到在哪输入,时间便已走到了0:00,既然都已经到第二天了,那不如好好写,写出点实用的东西来。

延伸

怎么让遮罩+弹窗的效果不那么单调?

原始效果:后面有一个遮罩,半透明灰色(background 设置 rgba),背景模糊(backdrop-filter:blur(10px),前面是一个弹窗,加一些边框阴影(box-shadow),居中显示(fixed定位通过 left:50%;top:50% 使弹窗左上角居中,其中百分比相对于父元素,设置 transform: translate(-50%, -50%) 进行补偿性位移,其中百分比相对于自身),用户调用封装好的函数 showToast 立即显示遮罩和弹窗。遮罩的主要作用是防止弹窗以外区域被点击。

进阶效果:showToast的时候增加一个动画过渡——弹窗上浮。 在这里将遮罩和弹窗的样式抽象成两个class: .mask.modal。 样式代码如下所示。其中的一个细节是定义动画帧的时候用上了calc()属性,功能顾名思义就是计算一个表达式,它可以混用百分比和像素单位,从而能够让我通过transform: translate(-50%, calc(-50% + 30px));精准地控制动画起点弹窗处在居中靠下偏移 30px 的位置。

.mask {
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
  position: fixed;
  backdrop-filter: blur(10px);
  z-index: 100;
  background: #33333333;
}
.modal {
  box-shadow: 0 2px 10px var(--shadow-color);
  min-width: 200px;
  max-width: 300px;
  min-height: 150px;
  position: sticky;
  top: 50%;
  left: 50%;
  z-index: 101;
  padding: 10px;
  color: var(--text-color);
  background: var(--bg-color);
  word-break: break-all;
  animation: float ease-out 600ms forwards;
}

@keyframes float {
  from {
    transform: translate(-50%, calc(-50% + 30px));
    opacity: 0;
  }
  to {
    transform: translate(-50%, -50%);
    opacity: 1;
  }
}

参考资料

position - CSS(层叠样式表) | MDN (mozilla.org)