fixed布局踩坑引发的深思
「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
故事背景
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;
}
}