使用背景属性实现酷炫悬停效果

Avatar of Temani Afif
Temani Afif 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

不久前,Geoff 撰写了一篇 关于酷炫悬停效果的文章。该效果依赖于 CSS 伪元素、变换和过渡的组合。许多评论表明,可以使用背景属性实现相同的效果。Geoff 提到这是他最初的想法,这也是我当时的想法。我并不是说他最终选择的伪元素不好,但了解实现相同效果的不同方法总是一件好事。

酷炫悬停效果系列

  1. 使用背景属性实现酷炫悬停效果(您现在所处位置!
  2. 使用 CSS 文本阴影实现酷炫悬停效果
  3. 使用背景裁剪、蒙版和 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 简写中定义大小时,此值是必需的。此外,我们无论如何都需要它来实现我们的悬停效果。

我们还需要在悬停时更新位置。我们可以分两步完成此操作

  1. 在鼠标悬停时从右侧增加大小。
  2. 在鼠标移出时从左侧减小大小。

为此,我们还需要在悬停时更新 background-position

我们在代码中添加了两件事

  • 悬停时的 background-position 值为 right
  • background-positiontransition-duration0s

这意味着,在悬停时,我们将 background-positionleft(请参阅,我们需要该值!)立即更改为 right,以便背景的大小将从右侧增加。然后,当鼠标光标离开链接时,过渡将反向播放,从 rightleft,使其看起来像是我们从左侧减小了背景的大小。我们的悬停效果完成了!

但是您说过我们只需要三个声明,而现在有四个。

没错,观察得很好。leftright 值可以分别更改为 0 0100% 0;并且由于我们的渐变默认情况下已经是全高的,因此我们可以使用 0100%

.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

我们需要更复杂的过渡才能实现此效果。让我们看一下分步说明以了解正在发生的事情。

Diagram showing the hover effect in three pieces.
最初,一个固定高度、全宽的渐变在视野之外。然后我们将渐变向右移动以覆盖底部。最后,我们将渐变的大小从固定高度增加到 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-sizebackground-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 个是在没有伪元素的情况下完成的。我们在本文中介绍的四个效果仅仅是冰山一角!