不久前,Geoff 撰写了一篇 关于酷炫悬停效果的文章。该效果依赖于 CSS 伪元素、变换和过渡的组合。许多评论表明,可以使用背景属性实现相同的效果。Geoff 提到这是他最初的想法,这也是我当时的想法。我并不是说他最终选择的伪元素不好,但了解实现相同效果的不同方法总是一件好事。
酷炫悬停效果系列
- 使用背景属性实现酷炫悬停效果(您现在所处位置!)
- 使用 CSS 文本阴影实现酷炫悬停效果
- 使用背景裁剪、蒙版和 3D 实现酷炫悬停效果
在这篇文章中,我们将重新设计该悬停效果,并将其扩展到其他仅使用 CSS background
属性的悬停效果类型。
您可以在该演示中看到 background
属性的工作原理,以及我们如何使用自定义属性和 calc()
函数来实现更多功能。我们将学习如何组合所有这些属性,以便最终得到经过良好优化的代码!
悬停效果 #1
让我们从第一个效果开始,即 Geoff 在其文章中详细介绍的效果的复制。实现该效果的代码如下所示
.hover-1 {
background: linear-gradient(#1095c1 0 0) var(--p, 0) / var(--p, 0) no-repeat;
transition: .4s, background-position 0s;
}
.hover-1:hover {
--p: 100%;
color: #fff;
}
如果我们省略颜色过渡(可选),则只需要三个 CSS 声明即可实现该效果。您可能会对代码的简洁感到惊讶,但您会看到我们是如何做到的。
首先,让我们从一个简单的 background-size
过渡开始
我们正在对线性渐变的大小进行动画,从 0 100%
到 100% 100%
。这意味着宽度从 0
变为 100%
,而背景本身保持完整高度。到目前为止,没什么复杂的。
让我们开始优化。我们首先转换渐变以仅使用一次颜色
background-image: linear-gradient(#1095c1 0 0);
语法可能看起来有点奇怪,但我们告诉浏览器将一种颜色应用于两个颜色停止点,这足以在 CSS 中定义渐变。两个颜色停止点都是 0
,因此浏览器会自动将最后一个设为 100%
并使用相同的颜色填充我们的渐变。快捷方式,FTW!
使用 background-size
,我们可以省略高度,因为渐变默认情况下是全高的。我们可以从 background-size: 0
过渡到 background-size: 100%
。
.hover-1 {
background-image: linear-gradient(#1095c1 0 0);
background-size: 0;
background-repeat: no-repeat;
transition: .4s;
}
.hover-1:hover {
background-size: 100%;
}
让我们引入一个自定义属性以避免重复 background-size
.hover-1 {
background-image: linear-gradient(#1095c1 0 0);
background-size: var(--p, 0%);
background-repeat: no-repeat;
transition: .4s;
}
.hover-1:hover {
--p: 100%;
}
我们最初没有定义 --p
,因此将使用回退值(在本例中为 0%
)。在悬停时,我们定义一个替换回退值的值(100%
)。
现在,让我们使用简写版本组合所有背景属性,得到
.hover-1 {
background: linear-gradient(#1095c1 0 0) left / var(--p, 0%) no-repeat;
transition: .4s;
}
.hover-1:hover {
--p: 100%;
}
我们越来越接近了!请注意,我已引入了一个 left
值(用于 background-position
),在 background
简写中定义大小时,此值是必需的。此外,我们无论如何都需要它来实现我们的悬停效果。
我们还需要在悬停时更新位置。我们可以分两步完成此操作
- 在鼠标悬停时从右侧增加大小。
- 在鼠标移出时从左侧减小大小。
为此,我们还需要在悬停时更新 background-position
我们在代码中添加了两件事
- 悬停时的
background-position
值为right
background-position
的transition-duration
为0s
这意味着,在悬停时,我们将 background-position
从 left
(请参阅,我们需要该值!)立即更改为 right
,以便背景的大小将从右侧增加。然后,当鼠标光标离开链接时,过渡将反向播放,从 right
到 left
,使其看起来像是我们从左侧减小了背景的大小。我们的悬停效果完成了!
但是您说过我们只需要三个声明,而现在有四个。
没错,观察得很好。left
和 right
值可以分别更改为 0 0
和 100% 0
;并且由于我们的渐变默认情况下已经是全高的,因此我们可以使用 0
和 100%
。
.hover-1 {
background: linear-gradient(#1095c1 0 0) 0 / var(--p, 0%) no-repeat;
transition: .4s, background-position 0s;
}
.hover-1:hover {
--p: 100%;
background-position: 100%;
}
看看 background-position
和 --p
如何使用相同的值?现在我们可以将代码减少到三个声明
.hover-1 {
background: linear-gradient(#1095c1 0 0) var(--p, 0%) / var(--p,0%) no-repeat;
transition: .4s, background-position 0s;
}
.hover-1:hover {
--p: 100%;
}
自定义属性 --p
同时定义了背景位置和大小。在悬停时,它也会同时更新这两个属性。这是一个完美的用例,展示了自定义属性如何帮助我们减少冗余代码并避免多次编写属性。我们使用自定义属性定义设置,并且仅在悬停时更新后者。
但是 Geoff 描述的效果正好相反,从左开始到右结束。当我们似乎无法依赖同一个变量时,我们该怎么做呢?
我们仍然可以使用一个变量,并稍微更新我们的代码以实现相反的效果。我们想要的是从 100%
到 0%
而不是从 0%
到 100%
。我们有一个 100%
的差异,我们可以使用 calc()
来表示,如下所示
.hover-1 {
background: linear-gradient(#1095c1 0 0) calc(100% - var(--p,0%)) / var(--p,0%) no-repeat;
transition: .4s, background-position 0s;
}
.hover-1:hover {
--p: 100%;
}
--p
将从 0%
更改为 100%
,但由于 calc()
,背景位置将从 100%
更改为 0%
。
我们仍然有三个声明和一个自定义属性,但效果不同。
在我们转向下一个悬停效果之前,我想强调一些您可能已经注意到的重要内容。在处理自定义属性时,我使用 0%
(带单位)而不是无单位的 0
。当自定义属性单独使用时,无单位的零可能会起作用,但在 calc()
内部会失败,在 calc()
内部我们需要显式定义单位。我可能需要另一篇文章来解释这个怪癖,但请始终记住在处理自定义属性时添加单位。我在 StackOverflow 上有两个答案(这里 和 这里)更详细地介绍了这一点。
悬停效果 #2
我们需要更复杂的过渡才能实现此效果。让我们看一下分步说明以了解正在发生的事情。

100%
以覆盖整个元素。我们首先进行 background-position
过渡,然后进行 background-size
过渡。让我们将其转换为代码
.hover-2 {
background-image: linear-gradient(#1095c1 0 0);
background-size: 100% .08em; /* .08em is our fixed height; modify as needed. */
background-position: /* ??? */;
background-repeat: no-repeat;
transition: background-size .3s, background-position .3s .3s;
}
.hover-2:hover {
transition: background-size .3s .3s, background-position .3s;
background-size: 100% 100%;
background-position: /* ??? */;
}
请注意两个过渡值的使用。在悬停时,我们需要首先更改位置,然后更改大小,这就是我们向大小添加延迟的原因。在鼠标移出时,我们执行相反的操作。
现在的问题是:我们对 background-position
使用什么值?我们在上面留空了这些值。background-size
值很简单,但 background-position
的值并非如此。如果我们保留实际配置,我们将无法移动渐变。
我们的渐变宽度等于 100%
,因此我们无法在 background-position
上使用百分比值来移动它。
与 background-position
一起使用的百分比值始终很麻烦,尤其是在您第一次使用它们时。它们的行为不直观,但定义明确且易于理解,如果我们了解其背后的逻辑。我认为需要另一篇文章才能全面解释其工作原理,但这里有 我在 Stack Overflow 上发布的另一个“长篇”解释。我建议花几分钟阅读该答案,您以后会感谢我的!
诀窍是将宽度更改为与 100%
不同的值。让我们使用 200%
。我们不担心背景超出元素,因为无论如何溢出都是隐藏的。
.hover-2 {
background-image: linear-gradient(#1095c1 0 0);
background-size: 200% .08em;
background-position: 200% 100%;
background-repeat: no-repeat;
transition: background-size .3s, background-position .3s .3s;
}
.hover-2:hover {
transition: background-size .3s .3s, background-position .3s;
background-size: 200% 100%;
background-position: 100% 100%;
}
以下是我们得到的结果
现在是时候优化我们的代码了。如果我们采用从第一个悬停效果中学到的想法,我们可以使用简写属性并编写更少的声明来使其工作
.hover-2 {
background:
linear-gradient(#1095c1 0 0) no-repeat
var(--p, 200%) 100% / 200% var(--p, .08em);
transition: .3s var(--t, 0s), background-position .3s calc(.3s - var(--t, 0s));
}
.hover-2:hover {
--p: 100%;
--t: .3s;
}
我们将所有背景属性使用简写版本组合在一起,然后使用 --p
来表示我们的值。大小从 .08em
更改为 100%
,位置从 200%
更改为 100%
我还使用另一个变量 --t
来优化过渡属性。在鼠标悬停时,我们将其设置为 .3s
值,这将给我们
transition: .3s .3s, background-position .3s 0s;
在鼠标移出时,--t
未定义,因此将使用回退值
transition: .3s 0s, background-position .3s .3s;
我们不应该在
transition
中包含background-size
吗?
这确实是我们可以进行的另一个优化。如果我们没有指定任何属性,则意味着“所有”属性,因此过渡被定义为“所有”属性(包括 background-size
和 background-position
)。然后它再次为 background-position
定义,这类似于为 background-size
,然后 background-position
定义它。
“类似”与说某物是“相同”不同。如果您在悬停时更改更多属性,您会看到差异,因此最后一个优化在某些情况下可能不适用。
我们还能优化代码,只使用一个自定义属性吗?
可以!Ana Tudor 分享了一篇很棒的文章,解释了如何创建DRY切换,其中一个自定义属性可以更新多个属性。我在这里就不详细介绍了,但是我们的代码可以这样修改
.hover-2 {
background:
linear-gradient(#1095c1 0 0) no-repeat
calc(200% - var(--i, 0) * 100%) 100% / 200% calc(100% * var(--i, 0) + .08em);
transition: .3s calc(var(--i, 0) * .3s), background-position .3s calc(.3s - calc(var(--i, 0) * .3s));
}
.hover-2:hover {
--i: 1;
}
--i
自定义属性最初未定义,因此使用回退值 0
。但是,在悬停时,我们将 0
替换为 1
。您可以对这两种情况进行计算,并获得每种情况的值。您可以将该变量视为一个“开关”,它可以在悬停时一次更新所有值。
同样,我们又回到了仅三个声明来实现一个非常酷的悬停效果!
悬停效果 #3
我们将为此效果使用两个渐变而不是一个。我们将看到组合多个渐变是创建花哨悬停效果的另一种方法。
以下是我们正在执行的操作的示意图

以下是 CSS 中的实现方式
.hover-3 {
background-image:
linear-gradient(#1095c1 0 0),
linear-gradient(#1095c1 0 0);
background-repeat: no-repeat;
background-size: 50% .08em;
background-position:
-100% 100%,
200% 0;
transition: background-size .3s, background-position .3s .3s;
}
.hover-3:hover {
background-size: 50% 100%;
background-position:
0 100%,
100% 0;
transition: background-size .3s .3s, background-position .3s;
}
代码与我们之前介绍的其他悬停效果几乎相同。唯一的区别是我们有两个渐变和两个不同的位置。位置值可能看起来很奇怪,但同样,这与 CSS 中 background-position
属性的工作原理有关,因此如果您想深入了解详细信息,我强烈建议您阅读我的Stack Overflow 回答。
现在让我们优化!您现在应该了解了思路——我们使用简写属性、自定义属性和 calc()
来整理代码。
.hover-3 {
--c: no-repeat linear-gradient(#1095c1 0 0);
background:
var(--c) calc(-100% + var(--p, 0%)) 100% / 50% var(--p, .08em),
var(--c) calc( 200% - var(--p, 0%)) 0 / 50% var(--p, .08em);
transition: .3s var(--t, 0s), background-position .3s calc(.3s - var(--t, 0s));
}
.hover-3:hover {
--p: 100%;
--t: 0.3s;
}
我添加了一个额外的自定义属性 --c
,它定义了渐变,因为在两个地方都使用了相同的渐变。
在该演示中,我使用 50.1%
而不是 50%
作为背景大小,因为这可以防止渐变之间出现间隙。出于类似原因,我还向位置添加了 1%
。
让我们通过使用开关变量进行第二次优化
.hover-3 {
--c: no-repeat linear-gradient(#1095c1 0 0);
background:
var(--c) calc(-100% + var(--i, 0) * 100%) 100% / 50% calc(100% * var(--i, 0) + .08em),
var(--c) calc( 200% - var(--i, 0) * 100%) 0 / 50% calc(100% * var(--i, 0) + .08em);
transition: .3s calc(var(--i, 0) * .3s), background-position .3s calc(.3s - var(--i, 0) * .3s);
}
.hover-3:hover {
--i: 1;
}
您是否开始看到这里的模式了?并不是说我们正在制作的效果很难。更多的是代码优化的“最后一步”。我们首先使用大量属性编写冗长的代码,然后遵循简单的规则(例如,使用简写、删除默认值、避免冗余值等)对其进行简化,以尽可能地简化代码。
悬停效果 #4
我将提高最后一个效果的难度级别,但您已经从其他示例中了解了足够的信息,我怀疑您不会遇到任何问题。
此悬停效果依赖于两个圆锥渐变和更多计算。

最初,我们在步骤 1 中使两个渐变的尺寸都为零。我们在步骤 2 中增加每个渐变的大小。我们继续增加它们的宽度,直到它们完全覆盖元素,如步骤 3 所示。之后,我们将其滑动到底部以更新其位置。这是悬停效果的“神奇”部分。由于两个渐变都将使用相同的颜色,因此在步骤 4 中更改其位置不会产生视觉差异——但当我们在步骤 5 中鼠标移出时减小尺寸时,我们将看到差异。
如果比较步骤 2 和步骤 5,您可以看到我们有不同的倾斜度。让我们将其转换为代码
.hover-4 {
background-image:
conic-gradient(/* ??? */),
conic-gradient(/* ??? */);
background-position:
0 0,
100% 0;
background-size: 0% 200%;
background-repeat: no-repeat;
transition: background-size .4s, background-position 0s;
}
.hover-4:hover {
background-size: /* ??? */ 200%;
background-position:
0 100%,
100% 100%;
}
位置非常清楚。一个渐变从左上角 (0 0
) 开始,到左下角 (0 100%
) 结束,而另一个渐变从右上角 (100% 0
) 开始,到右下角 (100% 100%
) 结束。
我们正在对背景位置和大小使用 transition
来显示它们。我们只需要 background-size
的过渡值。并且像之前一样,background-position
需要立即更改,因此我们为过渡的持续时间分配了 0s
值。
对于大小,两个渐变都需要宽度为 0 且高度为元素高度的两倍 (0% 200%
)。我们稍后将了解它们在悬停时的大小如何变化。让我们首先定义渐变配置。
下图说明了每个渐变的配置

请注意,对于第二个渐变(以绿色表示),我们需要知道高度才能在我们要创建的 conic-gradient
中使用它。出于这个原因,我将添加一个 line-height
来设置元素的高度,然后尝试在遗漏的圆锥渐变值中使用相同的值。
.hover-4 {
--c: #1095c1;
line-height: 1.2em;
background-image:
conic-gradient(from -135deg at 100% 50%, var(--c) 90deg, #0000 0),
conic-gradient(from -135deg at 1.2em 50%, #0000 90deg, var(--c) 0);
background-position:
0 0,
100% 0;
background-size: 0% 200%;
background-repeat: no-repeat;
transition: background-size .4s, background-position 0s;
}
.hover-4:hover {
background-size: /* ??? */ 200%;
background-position:
0 100%,
100% 100%;
}
我们剩下的最后一件事是确定背景的大小。直觉上,我们可能会认为每个渐变都需要占据元素宽度的一半,但这实际上还不够。

50%
作为两个渐变的 background-size
值,则会留下一个很大的间隙。我们得到一个等于高度的间隙,因此我们实际上需要做的是在悬停时将每个渐变的大小增加 *一半的高度*,以便它们覆盖整个元素。
.hover-4:hover {
background-size: calc(50% + .6em) 200%;
background-position:
0 100%,
100% 100%;
}
以下是按照前面示例对其进行优化后的结果
.hover-4 {
--c: #1095c1;
line-height: 1.2em;
background:
conic-gradient(from -135deg at 100% 50%, var(--c) 90deg, #0000 0)
0 var(--p, 0%) / var(--s, 0%) 200% no-repeat,
conic-gradient(from -135deg at 1.2em 50%, #0000 90deg, var(--c) 0)
100% var(--p, 0%) / var(--s, 0%) 200% no-repeat;
transition: .4s, background-position 0s;
}
.hover-4:hover {
--p: 100%;
--s: calc(50% + .6em);
}
只有一个自定义属性的版本怎么样?
我留给您来完成!在查看了四个类似的悬停效果后,您应该能够将最终优化简化为单个自定义属性。在评论区分享您的作品!没有奖品,但我们最终可能会得到不同的实现和想法,从而使每个人都受益!
在结束之前,让我分享一下Ana Tudor 制作的最后一个悬停效果的版本。这是一个改进!但请注意,由于已知的错误,它缺少 Firefox 支持。尽管如此,这是一个很棒的想法,展示了如何将渐变与混合模式结合起来创建更酷的悬停效果。
总结
我们制作了四个超级酷的悬停效果!即使它们是不同的效果,它们也都采用了相同的方法,即使用 CSS background
属性、自定义属性和 calc()
。不同的组合使我们能够创建不同的版本,所有这些都使用相同的技术,使我们能够获得简洁、可维护的代码。
如果您想获得一些想法,我创建了一个包含 500 个(是的,500 个!)悬停效果的合集,其中 400 个是在没有伪元素的情况下完成的。我们在本文中介绍的四个效果仅仅是冰山一角!
我很高兴最近的文章都围绕核心前端主题展开。我担心该网站的关注点正在发生巨大变化。
写得不错!
但是,我认为绝对值得注意的是,CSS 背景任何内容都可能导致我在此处使用 Safari 时注意到的延迟或抖动。原因是背景属性会导致重绘,而这很快就会变得昂贵。使用具有绝对或固定位置的伪属性可以轻松避免此问题,并使动画以流畅的 60fps 运行。
干杯!
如果您能将这些动画与 tailwind css 一起使用就好了,但
--c
--s
变量的使用与 tailwind 类集成起来很复杂。你好,
页面顶部所有四个一起显示时,第四个悬停效果有错误。悬停时,底部中间会出现一个小小的白色三角形。
但在您解释第四个效果的地方,没有问题。
为什么?
有什么不同?
很棒的文章。
你好,
不错的文章,需要好好消化一下。对于第一个悬停效果,我想知道为什么必须添加
background-repeat: no-repeat;
才能使其正常工作?