您是否知道那种有人头从圆圈或洞里探出来的那种效果?著名的猪小弟动画片中他挥手告别,从一系列红环中探出来的场景就是完美的例子,而 Kilian Valkhof 实际上在 CSS-Tricks 上重新制作了这个动画。
我有一个类似的想法,但采用不同的方法,并加入了一些动画。我认为这非常实用,并且可以制作一个简洁的悬停效果,您可以将其用于自己的头像等。
看到了吗?我们将制作一个缩放动画,让头像看起来像是从它所在的圆圈中弹出。很酷,对吧?先别看代码,让我们一步一步地一起构建这个动画。
HTML:只有一个元素
如果您还没有查看演示代码,并且想知道这需要多少个 div
,那么现在就停下来吧,因为我们的标记只是一个简单的图像元素。
<img src="" alt="">
是的,只有一个元素!这个练习的难点在于使用尽可能少的代码。如果您已经 关注我一段时间了,您应该已经习惯了这一点。我努力寻找可以采用尽可能少、最易维护的代码实现的 CSS 解决方案。
我在 CSS-Tricks 上写了一系列文章 探讨了使用包含单个元素的相同 HTML 标记实现不同悬停效果的方法。我详细介绍了渐变、遮罩、剪裁、轮廓,甚至布局技巧。我强烈建议您查看这些内容,因为我将在本文中重新使用其中的许多技巧。
一个具有透明背景的正方形图像文件最适合我们的操作。如果您想使用这个文件,请使用我正在使用的这个文件。

我希望看到许多使用真实图像实现此效果的示例——因此,完成后请在评论区分享您的最终结果,以便我们共同创建一个合集!
在开始 CSS 之前,让我们先分析一下效果。图像在悬停时变大,因此我们一定会使用 transform: scale()
。头像后面有一个圆圈,径向渐变可以解决这个问题。最后,我们需要一种方法来在圆圈底部创建一个边框,让它看起来像是头像在圆圈后面。
让我们开始吧!
缩放效果
让我们先添加变换
img {
width: 280px;
aspect-ratio: 1;
cursor: pointer;
transition: .5s;
}
img:hover {
transform: scale(1.35);
}
还不算复杂,对吧?让我们继续。
圆圈
我们说背景将是一个径向渐变。这很完美,因为我们可以创建径向渐变颜色之间的硬性停止,这使得它看起来像我们正在使用实线绘制一个圆圈。
img {
--b: 5px; /* border width */
width: 280px;
aspect-ratio: 1;
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
#C02942 calc(100% - var(--b)) 99%,
#0000
);
cursor: pointer;
transition: .5s;
}
img:hover {
transform: scale(1.35);
}
注意那里使用的 CSS 变量 --b
。它代表“边框”的厚度,实际上它只是用于定义径向渐变红色部分的硬性颜色停止。
下一步是玩玩悬停时渐变的大小。圆圈需要保持其大小,因为图像会增长。由于我们正在应用 scale()
变换,实际上我们需要减小圆圈的大小,否则它会随着头像一起放大。因此,当图像放大时,我们需要渐变缩小。
让我们先定义一个 CSS 变量 --f
来定义“缩放因子”,并使用它来设置圆圈的大小。我使用 1
作为默认值,因为这是图像和圆圈的初始缩放比例,我们从这里进行变换。
这是一个演示,说明了这个技巧。悬停以查看幕后发生了什么
我在 radial-gradient
中添加了第三种颜色,以更好地识别悬停时渐变的区域
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
#C02942 calc(100% - var(--b)) 99%,
lightblue
);
现在,我们必须将背景放置在圆圈的中心,并确保它占用了整个高度。我喜欢直接在 background
简写属性上声明所有内容,这样我们就可以添加背景定位,并确保它不会重复,只需在 radial-gradient()
后面添加这些值。
background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;
背景放置在中心 (50%
),宽度等于 calc(100%/var(--f))
,高度等于 100%
。
当 --f
等于 1
时,没有任何东西会缩放——再次说明,这是我们的初始缩放比例。同时,渐变占据了容器的整个宽度。当我们增加 --f
时,元素的大小会增加——由于 scale()
变换——并且渐变的大小会减小。
以下是将所有这些应用到我们的演示中后得到的结果
我们越来越接近了!我们在顶部获得了溢出效果,但我们仍然需要隐藏图像的底部部分,这样看起来像是它从圆圈中弹出,而不是位于圆圈前面。这是整个事情中最棘手的一部分,也是我们接下来要做的。
底部边框
我最初尝试使用 border-bottom
属性来解决这个问题,但我无法找到一种方法让边框的大小与圆圈的大小匹配。这是我能获得的最佳效果,您会立即发现它不对
实际解决方案是使用 outline
属性。是的,是 outline
,而不是 border
。在 之前的一篇文章中,我展示了 outline
的强大功能,以及如何使用它来创建酷炫的悬停效果。结合 outline-offset
,我们拥有了实现此效果所需的全部内容。
这个想法是在图像上设置一个 outline
,并调整它的偏移量以创建底部边框。偏移量将取决于缩放因子,就像渐变大小一样。
现在,我们有了底部“边框”(实际上是一个 outline
)与渐变创建的“边框”相结合,形成一个完整的圆圈。我们仍然需要隐藏 outline
的一部分(来自顶部和侧面),我们稍后会处理。
以下是迄今为止的代码,包括一些可以用来配置图像大小 (--s
) 和“边框”颜色 (--c
) 的 CSS 变量
img {
--s: 280px; /* image size */
--b: 5px; /* border thickness */
--c: #C02942; /* border color */
--f: 1; /* initial scale */
width: var(--s);
aspect-ratio: 1;
cursor: pointer;
border-radius: 0 0 999px 999px;
outline: var(--b) solid var(--c);
outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
var(--c) calc(100% - var(--b)) 99%,
#0000
) 50% / calc(100% / var(--f)) 100% no-repeat;
transform: scale(var(--f));
transition: .5s;
}
img:hover {
--f: 1.35; /* hover scale */
}
由于我们需要一个圆形的底部边框,因此我们在底部添加了一个 border-radius
,使 outline
匹配渐变的曲率。
在 outline-offset
上使用的计算比看起来要简单得多。默认情况下,outline
绘制在元素框的外部。在我们的例子中,我们需要它重叠元素。更确切地说,我们需要它遵循渐变创建的圆圈。

当我们缩放元素时,我们看到圆圈和边缘之间的空间。别忘了,这个想法是在缩放变换运行后保持圆圈的大小不变,这样就留下了用于定义轮廓偏移量的空间,如上图所示。
别忘了,第二个元素也被缩放了,因此我们的结果也被缩放了……这意味着我们需要将结果除以 f
来获得真实的偏移值
Offset = ((f - 1) * S/2) / f = (1 - 1/f) * S/2
我们添加了一个负号,因为我们需要轮廓从外部到内部。
Offset = (1/f - 1) * S/2
这是一个快速演示,展示了轮廓如何跟随渐变
您可能已经看到了,但我们仍然需要让底部轮廓重叠圆圈,而不是让它穿透圆圈。我们可以通过从偏移量中减去边框的大小来做到这一点
outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2) - var(--b));
现在,我们需要找到如何从轮廓中移除顶部部分。换句话说,我们只想要图像 outline
的底部部分。
首先,让我们使用填充在顶部添加空间,以帮助避免顶部重叠
img {
--s: 280px; /* image size */
--b: 5px; /* border thickness */
--c: #C02942; /* border color */
--f: 1; /* initial scale */
width: var(--s);
aspect-ratio: 1;
padding-block-start: calc(var(--s)/5);
/* etc. */
}
img:hover {
--f: 1.35; /* hover scale */
}
顶部填充没有特定的逻辑。这个想法是确保轮廓不触及头像的头部。我使用了元素的大小来定义这个空间,以始终保持相同的比例。
注意,我在 background
中添加了 content-box
值
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
var(--c) calc(100% - var(--b)) 99%,
#0000
) 50%/calc(100%/var(--f)) 100% no-repeat content-box;
我们需要这个,因为我们添加了填充,并且我们只希望背景设置为内容框,因此我们必须明确告诉背景在那里停止。
将 CSS 遮罩加入混合
我们到达了最后一步!我们只需要隐藏一些部分,就完成了。为此,我们将依靠 mask
属性,当然还有渐变。
这是一张图,说明了我们需要隐藏的部分,或者更准确地说,我们需要显示的部分

左边的图像是我们目前拥有的,右边的图像是我们想要的。绿色部分说明了我们必须应用于原始图像的遮罩才能获得最终结果。
我们可以识别遮罩的两个部分
- 底部有一个圆形部分,它与我们用来创建头像后面圆圈的径向渐变具有相同的尺寸和曲率
- 顶部有一个矩形,它覆盖了轮廓内的区域。注意轮廓在顶部位于绿色区域的外部——这是最重要的部分,因为它允许剪切轮廓,使只有底部部分可见。
这是我们最终的 CSS。
img {
--s: 280px; /* image size */
--b: 5px; /* border thickness */
--c: #C02942; /* border color */
--f: 1; /* initial scale */
--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
width: var(--s);
aspect-ratio: 1;
padding-top: calc(var(--s)/5);
cursor: pointer;
border-radius: 0 0 999px 999px;
outline: var(--b) solid var(--c);
outline-offset: var(--_o);
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
var(--c) calc(100% - var(--b)) 99%,
#0000) var(--_g);
mask:
linear-gradient(#000 0 0) no-repeat
50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
radial-gradient(
circle closest-side,
#000 99%,
#0000) var(--_g);
transform: scale(var(--f));
transition: .5s;
}
img:hover {
--f: 1.35; /* hover scale */
}
让我们分解一下mask
属性。首先,注意background
属性中的类似radial-gradient()
在那里。我创建了一个新的变量--_g
来表示公共部分,使代码更简洁。
--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
mask:
radial-gradient(
circle closest-side,
#000 99%,
#0000) var(--_g);
接下来,里面还有一个linear-gradient()
。
--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
mask:
linear-gradient(#000 0 0) no-repeat
50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
radial-gradient(
circle closest-side,
#000 99%,
#0000) var(--_g);
这创建了蒙版的矩形部分。它的宽度等于径向渐变的宽度减去两倍的边框厚度。
calc(100% / var(--f) - 2 * var(--b))
矩形的高度等于元素大小的一半,即50%
。
我们还需要将线性渐变放置在水平中心 (50%
) 并从顶部偏移与轮廓偏移相同的距离。我创建了另一个 CSS 变量--_o
来表示之前定义的偏移量。
--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
这里令人困惑的一点是,我们需要对轮廓进行负偏移(将其从外部移动到内部),但对渐变进行正偏移(从顶部移动到底部)。所以,如果你想知道为什么我们要将偏移量--_o
乘以-1
,现在你知道了!
这是一个演示,说明蒙版的渐变配置。
将鼠标悬停在上面,看看一切是如何一起移动的。中间的方框说明了由两个渐变组成的蒙版层。想象一下,它是左侧图像的可见部分,你就会得到右侧的最终结果!
总结
哦,我们完成了!我们不仅得到了一种流畅的悬停动画,而且我们使用单个 HTML <img>
元素就完成了所有操作。仅仅只有它,不到 20 行 CSS 技巧!
当然,我们依靠了一些小技巧和数学公式来实现如此复杂的效果。但我们确切地知道该怎么做,因为我们事先确定了所需的元素。
如果我们允许自己使用更多 HTML,是否可以简化 CSS?当然可以。但我们在这里学习新的 CSS 技巧!这是一个很好的练习,可以探索 CSS 渐变、蒙版、outline
属性的行为、转换等等。如果你在任何时候感到迷茫,那么一定要查看我的系列文章,它使用了相同的通用概念。有时,查看更多示例和用例有助于更好地理解。
我将用一个最后一个演示结束文章,它使用了流行 CSS 开发人员的照片。别忘了用你自己的图片展示一个演示,这样我就可以将其添加到收藏中!
这是一篇写得非常棒的文章,它解释了一个我认为非常复杂的效果,但却描述得如此简单,以至于我只需快速浏览代码就能理解它是如何实现的!
这是一篇非常棒的文章,我非常喜欢,尤其是演示部分!
这是一篇非常棒的文章。太棒了。你的 CSS 技能让我深受启发。|这就是 CSS tricks 的知名之处。我希望你以后还会继续写作。祝好!非常感谢!