很多网页上那种头像列表,一个个圆溜溜地叠在一起,看着挺有设计感。但以前要实现这个,要么写死数值,要么得靠JavaScript去算。现在CSS新特性一个接一个地出来,完全可以靠几行代码搞出一个能根据容器大小自动调整重叠间距,甚至还有交互动画的版本。这篇文章就把从打底子到最终效果的完整流程掰开揉碎了讲清楚,全程干货,看完就能用。
开整前的底子
响应式重叠头像列表这个概念,简单说就是一组圆形的头像图片,它们之间有一部分是重叠的,而且这种重叠的程度不是写死的,会随着外部容器(比如屏幕宽度)的变化而动态调整。核心在于既要让它们挤在一起形成视觉上的层次感,又要确保在空间变化时不会溢出或者挤得乱七八糟,最终目的是让这个组件能自适应任何布局,同时保持好看和可交互。
先搭个架子:基础布局
要实现这个效果,第一步就是把图片排成一行。HTML结构非常简单,就是一堆<img>标签放在一个容器里,比如:
<div class="avatar-group">
<img src="头像1.jpg" alt="">
<img src="头像2.jpg" alt="">
<img src="头像3.jpg" alt="">
<img src="头像4.jpg" alt="">
<!-- 想要多少张就加多少张 -->
</div>要让它们排排坐,只需要给父容器一个display: flex;。接下来,把图片变成圆形的,并且让它们叠起来。这里用border-radius: 50%;把图片变成圆形,再用负的margin-right值把右边的图片往左边拉,产生重叠效果。比如:
.avatar-group {
display: flex;
}
.avatar-group img {
width: 100px; /* 先定个基础尺寸 */
border-radius: 50%;
margin-right: -20px; /* 负边距,让图片叠起来 */
}
.avatar-group img:last-child {
margin-right: 0; /* 最后一张图右边不留负边距,否则会拉过头 */
}这样操作之后,图片就挤在一起了,但能看到后面的图片边缘还露在外面,叠得不够“干净”。这里有一个小细节,负边距的数值是随便写的,比如-20px,如果容器大小变化,这个固定的数值就不够灵活了。
mask搞透明间隙:让重叠更高级
基础的负边距重叠虽然能叠,但叠在一起的时候,前面的图片会完全盖住后面的,看起来像是一张图。要想让它们之间有个透明的“缝隙”,看起来像是互相错开,就需要用到mask属性。mask就像是在图片上盖一层蒙版,蒙版透明的部分,图片就会透出下面的背景。
这里的目标是,从每张图片的右侧切掉一块圆形区域,这个圆形区域的位置要恰好落在下一张图片的中间,从而露出下面的背景。同时,这个切掉的区域要包含一个透明间隙。核心代码思路是这样:
/* 这里的值需要根据图片尺寸和重叠值来算,后面我们会让它变聪明 */
mask: radial-gradient(50% 50% at calc(150% - 20px), #0000 calc(100% + 10px), #000);这个radial-gradient在图片上画了一个圆形渐变。at calc(150% - 20px)定义了圆心的位置,150%代表下一张图片的中心,减去20px是因为有了负边距后,下一张图片实际位置更近了。#0000 calc(100% + 10px)的意思是,从圆心到半径的“100% + 10px”范围内都是透明(#0000),超出这个范围才显示图片(#000)。这里的10px就是我们想要的透明间隙的宽度。但这里用到的20px和10px,都还是固定值,不够智能。
让代码自己算数:响应式核心
真正的挑战来了,怎么让这个重叠量(也就是负边距)和切掉的区域大小,能根据容器宽度和图片数量自动算出来?这就要用到CSS的数学能力,以及两个超级好用的新特性:sibling-count() 和 容器查询。
整点花活儿:sibling-count() 数人头
margin-right的值不能是固定的。想象一下,容器宽度是100%,每张图片宽度是固定的--size,有N张图,那么总宽度就是N * --size。如果容器宽度比这个小,多出来的空间就需要用负边距来“吃掉”。这个负边距应该等于:(100% - N * --size) / (N - 1)。因为最后一张图不需要负边距,所以分母是N-1。
sibling-count()这个函数可以直接获取到当前元素在同一父元素下的兄弟元素总数,也就是N。这样,我们就可以写一个动态的公式:
.avatar-group img {
--size: 100px; /* 图片尺寸 */
--gap: 10px; /* 想要的透明间隙 */
margin-right: calc((100% - sibling-count() * var(--size)) / (sibling-count() - 1));
}这样,无论容器里放了多少张图,浏览器都能自动算出每个人该往左挪多少。但还没完,当容器足够宽时,这个计算结果可能会变成正数,导致图片之间出现空隙。我们希望的是图片只重叠或紧贴,不要分开。所以要用min()函数给它加个上限,最大不能超过我们想要的间隙(比如0,或一个很小的正值,用来在图片不重叠时留出一点间距)。可以用min()确保这个值不会大于--gap,或者为0:
margin-right: min((100% - sibling-count() * var(--size)) / (sibling-count() - 1), var(--gap));这行代码的意思就是:取计算出来的间距和--gap中的较小值。当容器变窄,计算结果为负数(需要重叠),min()会取这个负数;当容器足够宽,计算结果为正数(需要分开),min()会取较小的--gap,保证了图片不会分开太远。
容器查询出马:让mask也能动起来
把这个动态计算的margin-right值用到mask里时,出问题了。因为margin里的100%是相对于父容器的宽度,但mask里radial-gradient的%是相对于图片自身的尺寸。两个“100%”不是一回事。所以,mask里的计算公式需要知道父容器的宽度。
这里就需要容器查询。把.avatar-group注册成一个“容器”,然后就可以在mask里用100cqw(容器宽度的100%)来代替之前的100%了。
.avatar-group {
container-type: inline-size;
}
.avatar-group img {
--_m: min((100cqw - sibling-count() * var(--size)) / (sibling-count() - 1), var(--gap));
margin-right: var(--_m);
mask: radial-gradient(50% 50% at calc(150% + var(--_m)), #0000 calc(100% + var(--gap)), #000);
}这里用了一个自定义属性--_m来存储计算好的动态间距,然后同时用在margin-right和mask的圆心位置计算里。calc(150% + var(--_m))是因为有了动态间距,下一张图片的实际位置也跟着变了。现在,整个布局和切图效果就都能随着容器大小完美联动了。注意,最后一张图片要特殊处理,把mask和margin-right都移除,保证它完整显示。
左右开弓:双向兼容
上面的代码默认是从右边切图,如果想让图片从左边开始叠,效果相反,需要换一套代码。比如添加一个.reverse类,然后通过它来切换margin-left和对应的mask方向。
.avatar-group:not(.reverse) img {
mask: radial-gradient(50% 50% at calc(150% + var(--_m)), #0000 calc(100% + var(--gap)), #000);
margin-right: var(--_m);
}
.avatar-group.reverse img {
mask: radial-gradient(50% 50% at calc(-50% - var(--_m)), #0000 calc(100% + var(--gap)), #000);
margin-left: var(--_m);
}这样,通过切换.reverse类,就能轻松控制头像列表是向右堆叠还是向左堆叠,适应不同的设计需求。
让动画丝滑起来:交互加分
为了让用户鼠标划过时有反馈,可以加个动画。目标是鼠标悬停在某张图片上时,这张图“弹出来”,把重叠部分推开,让其他图片自动调整间距,并且整个过程要流畅。
最简单的想法是鼠标悬停时,让--_m这个动态间距变成0,让图片不再重叠。但这样最后一张图可能会被挤出容器。为了更优雅,需要调整悬停时的公式。
当一张图片被悬停时,我们希望所有没有被悬停的图片,它们之间的间距重新计算,要排除悬停图片和最后一张图的影响,确保整体不溢出。这个新公式稍微复杂点,需要推导。但重点是,我们可以用:has()选择器来精确地定位这种状态。
/* 当容器内存在悬停的图片时,选中所有未被悬停的图片 */
.avatar-group:has(:hover) img:not(:hover) {
--_m: min((100cqw - var(--gap) - sibling-count() * var(--size)) / (sibling-count() - 2), var(--gap));
}这个公式算出来的--_m是给所有“非悬停”图片的新间距,确保悬停的图片获得完整显示,而其他图片压缩得更紧,但又不至于溢出。为了让这个变化不是生硬的,而是平滑过渡,需要把--_m注册成一个CSS属性,并给它加上transition。
@property --_m {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
.avatar-group img {
transition: --_m .3s linear;
}经过这么一番操作,现在鼠标划过头像时,头像会优雅地推开旁边的图片,整个过程的间隙调整和切图效果都会平滑过渡,视觉效果瞬间拉满。最后再处理一下边缘图片的悬停逻辑,确保第一张和最后一张悬停时,不会因为公式计算出错而出现奇怪行为,整个交互就完美了。
现在这套东西,从布局到响应式,再到丝滑动画,全是用现代CSS一把梭哈出来的,不用写一行JS,兼容性好的同时,代码还贼拉简洁,赶紧动手试试吧。
