fixed布局踩坑引发的深思
故事背景
scene 1:前几天看到同事在处理一个页面 bug,大致是有一个输入组件需要点击后置顶,ta 设计成点击后应用 fixed 布局,设置了 top = 0,left = 0,但是看起来组件并没有贴到最顶层,查看页面元素,其中该组件的祖先元素设置了 transform 属性。
scene 2:当天晚上,一时兴起,我写了一个遮罩+弹窗提示的功能,遮罩和弹窗用到了fixed布局,其中遮罩实现了背景模糊的功能。本以为能让弹窗居中,却没有达到预期的效果,弹窗贴到了屏幕底部。
知识点
fixed 定位: 元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。
fixed
属性会创建新的层叠上下文。当元素祖先的transform
,perspective
或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 复制代码
原本我以为是这样的
结果是这样的
原因在上一节已经说了,那么怎样实现我想要的,弹窗居中的效果呢?
方案一:将里面的弹窗元素的 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; } }
作者:月藤
链接:https://juejin.cn/post/7025631867801960455