搞响应式布局的时候,是不是老碰上这种破事:屏幕宽度一缩,布局直接“咔吧”一下跳成另一个样子,那个生硬劲儿跟换了个页面似的。传统媒体查询或者容器查询确实好用,但一到临界点附近,内容不是挤成一团就是稀稀拉拉,看着就闹心。其实完全可以让元素像水一样,随着容器宽窄连续变化位置和大小,根本感觉不到那个生硬的切换点。
啥是流体
流体布局的核心就是让CSS属性值跟着容器宽度走,而不是在几个固定断点之间蹦迪。这里要认识两个狠角色:cqi单位和clamp()函数。cqi是容器查询里的相对单位,1cqi等于容器内联方向(通常就是宽度)的1%。clamp()则给属性值设个安全区间,中间那个值会根据容器宽度自动计算。举个栗子,想让一个标题在容器宽360px时字号3.5rem,宽1200px时字号5rem,中间所有宽度都平滑过渡,用clamp(3.5rem, 2.857rem + 2.857cqi, 5rem)就能搞定。那个中间表达式看着像天书,其实是解了个一元一次方程得出来的,后面细说。
老办法的坑
先看看传统路子有多僵硬。假设页面有个英雄区,宽屏时文字在右下角,背景图完整露出猫鼬的脑袋;手机窄屏时文字挪到左上角,背景图也切个特写。通常做法是写个容器查询,宽度小于800px就整个换一套CSS。这法子跑起来没问题,但试一下把浏览器窗口从1200px慢慢拖到360px,会发现文字和背景图在中途某个宽度突然“啪”地跳过去,之前的位置跟之后的位置之间差了老远一段空白。更闹心的是,如果断点设在800px,那799px和801px的体验完全两码事,用户一旋转手机可能就踩到那个坎儿上。
开干 流体替换
来动手把那个生硬的断点改造成丝滑过渡。原始结构就一个div包着文字:
<div id="hero">
<div class="details">
<h1>LookOut</h1>
<p>Eagle Defense System</p>
</div>
</div>宽屏时文字用绝对定位,top:220px; left:565px;,h1字号5rem,p字号2.5rem。背景图挂在伪元素上,background-position-x:0; background-size:auto 589px;。窄屏(容器宽≤800px)时文字top:50px; left:20px;,字号分别缩到3.5rem和2rem,背景图往左挪到-310px,往上挪到-25px,尺寸放大到710px高。
现在要把这六七个属性全部改成随宽度连续变化。需要知道容器的最小宽度360px和最大宽度1200px,以及每个属性在两个极端宽度下的值。比如top属性,360px时是50px,1200px时是220px。解方程组:
f + 3.6v = 50 (360px宽时1cqi = 360/100 = 3.6px)
f + 12v = 220 (1200px宽时1cqi = 12px)算出来v = 20.238,f = -22.857,所以中间表达式为20.238cqi - 22.857px。套进clamp:
top: clamp(50px, 20.238cqi - 22.857px, 220px);同理left从20px变到565px,解得left: clamp(20px, 64.881cqi - 213.571px, 565px);。h1字号从3.5rem变到5rem,注意这里rem和cqi的换算:1200px宽时1cqi=12px,根字号默认16px,12/16=0.75rem;360px宽时1cqi=3.6px,3.6/16=0.225rem。解方程f + 0.75v = 5和f + 0.225v = 3.5,得到font-size: clamp(3.5rem, 2.857rem + 2.857cqi, 5rem);。背景图的background-position-x从-310px到0px,直接解cqi方程:360px时-310px,1200px时0px,得到clamp(-310px, 36.905cqi - 442.857px, 0px)。background-size高度从710px变到589px,注意这里递减:宽容器589px,窄容器710px,表达式是clamp(589px, 761.857px - 14.405cqi, 710px)。
把所有这些clamp表达式写进CSS后,原来那个@container查询块可以直接删掉。现在拖拽窗口边缘,文字和背景图会像抹了油一样连续滑动,根本找不到一个“突变点”。但有个小隐患:容器本身设了min-width:360px和max-width:1200px,理论上clamp永远不会碰到边界,可万一哪天改了容器限制,边界就没了。所以留着clamp当保险杠,别手贱删掉。
改路径 绕开障碍
流体跑起来虽然顺,但文字轨迹是一条直线,直接从猫鼬脑袋上碾过去。虽然图片里的猫鼬不会真疼,但看着膈应。解决方法很简单:让背景图移动得更快一些,早点让开位置。原来背景图从360px才开始往左跑,现在改成从450px宽的时候就完全挪到最左边。重新算background-position-x,起点450px对应-310px,终点1200px对应0px,解方程得到clamp(-310px, 41.333cqi - 496px, 0px)。背景图横向位移提前了,但文字路径还是直线,绕不开。
要想让文字拐个弯,得让top值走两条不同斜率的直线,然后取其中较小的那个。定义两个自定义属性:
--top-a: calc(32.143cqi - 165.714px); /* 陡线,宽屏时220px,窄屏时-50px */
--top-b: calc(20px + 8.333cqi); /* 缓线,宽屏时120px,窄屏时50px */
top: clamp(50px, max(var(--top-a), var(--top-b)), 220px);在容器宽度小于780px左右时,--top-a的值比--top-b大,max选中的是--top-a;宽度超过780px后,--top-b反超,文字开始沿着缓线移动。整个路径就像折了一下,刚好从猫鼬脑袋旁边滑过去。这两条线的参数不用算得特别精确,打开浏览器开发者工具,边拖边调,直到视觉上满意为止。
搞不定的时候 上JS
纯CSS的clamp只能做线性插值,没法搞贝塞尔曲线那种非线性的丝滑拐弯。而且calc()只能产出带单位的长度值,想根据容器宽度改变透明度或旋转角度?纯CSS办不到。这时候得请JavaScript来救场。写几行监听代码,拿到容器宽度,算出一个0到1之间的比例,塞进CSS自定义属性里,然后用这个比例去驱动任何CSS属性。比如让文字沿着二次贝塞尔曲线移动:
const hero = document.getElementById('hero');
const observer = new ResizeObserver(entries => {
const width = entries[0].contentRect.width;
// 假设最小360px,最大1200px,算出进度t
let t = (width - 360) / (1200 - 360);
t = Math.min(1, Math.max(0, t));
// 二次贝塞尔曲线,控制点让路径先缓后急
const bezierT = t * t;
const top = 50 + (220 - 50) * bezierT;
hero.style.setProperty('--dynamic-top', top + 'px');
});
observer.observe(hero);然后在CSS里直接用var(--dynamic-top)。这样透明度、旋转角度、甚至背景模糊都能跟着容器宽度非线性变化,自由度直接拉满。不过得注意,ResizeObserver稍微有点性能开销,别在一个页面上挂几十个。
混搭才是王道
不是说用了流体就必须扔掉所有断点。有些设计就是需要明确切换,比如导航栏在小屏幕下变成汉堡菜单,这种布局结构上的突变用媒体查询或容器查询更合适。完全可以流体和断点混着用:背景图和文字位置用clamp连续变化,而文字是从两列布局变成一列堆叠,那就用容器查询在某个宽度直接改grid-template-columns。流体管“量变”,断点管“质变”,各干各的活儿,不打架。
