用background属性实现炫酷的hover效果【译文】

395 阅读13分钟

原文链接:css-tricks.com/cool-hover-…

前段时间,Geoff 写了一篇关于 hover 效果的 文章,里面用到的实现方式是基于 CSS 伪元素、transformtransition 的组合。很多评论表明,使用 background 属性也可以达到同样的效果。Geoff 提到这是他最初的想法,我也是这么想的。并不是说他使用伪元素不好,知道不同的方法能够达到相同的效果是不是更棒~~

酷炫悬停效果的实现方式:

  1. 使用 background 属性的悬停效果 ( you are here! )
  2. 使用 CSS Text Shadow 实现酷的悬停效果
  3. 使用 Background Clipping, Masks, and 3D 实现炫酷悬停效果

在这篇文章中,我们将重新制作悬停效果,但我们换一种方式:使用 CSS background 属性。

CodePen 在线预览地址

1.gif

掘金无法内嵌 CodePen,只能我手动录视频再转 gif ,强大的掘金,求支持呀,iframe CodePen 是多么棒的体验

第一个 hover 效果

我们从第一个效果开始,代码如下:

.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;
}

如果省略了过渡效果 transition ,那我们只需要三个 CSS 声明就可以实现这个效果。代码如此少,有没有感到惊讶,下面我们来一步步实现:

首先,我们从一个简单的背景大小转换开始

.hover-1 {
  background-image: linear-gradient(#1095c1,#1095c1);
  background-size: 0 100%;
  background-repeat: no-repeat;
  transition: .4s;
}
.hover-1:hover {
  background-size: 100% 100%;
}

body {
  height: 100vh;
  margin: 0;
  display: grid;
  gap: 20px;
  place-content: center;
}
h3 {
  font-family: system-ui, sans-serif;
  font-size: 3rem;
  margin:0;
  cursor: pointer;
  padding: 0 .07em;
}

我们将线性渐变(linear-gradient)的大小从 0 100% 过渡到 100% 100%。这意味着宽度从 0 到 100%,而背景本身保持在全高度。

接下来我们开始优化。首先,我们将 gradient 转换为只使用一次颜色:

background-image: linear-gradient(#1095c1 0 0);

语法可能看起来有点奇怪,但足够定义 CSS 中的渐变了,两个颜色都是 0,所以浏览器自动将最后一个设置为 100%,并用相同的颜色填充渐变。称之为:FTW。

对于 background-size,可以忽略高度,因为渐变默认全高度。我们让它从 0 过渡到 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% 来替换 0%。

现在,让我们结合所有的 background 属性,简化一下代码:

.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 简写中定义 size 时是必须设置位置的。另外,我们需要它来实现我们的悬停效果。

我们还需要更新悬停时的位置,可以通过下面两步来实现:

  1. 在鼠标悬停时,从右侧开始增加大小。
  2. 在鼠标离开时,从左侧开始减小大小。

为此,我们需要更新悬停时的 background-position:

.hover-1 {
  background: linear-gradient(#1095c1 0 0) left / var(--p,0) no-repeat;
  transition: .4s,background-position 0s;
}
.hover-1:hover {
  --p: 100%;
  background-position: right;
}

body {
  height: 100vh;
  margin: 0;
  display: grid;
  gap: 20px;
  place-content: center;
}
h3 {
  font-family: system-ui, sans-serif;
  font-size: 3rem;
  margin:0;
  cursor: pointer;
  padding: 0 .07em;
}

上面的代码中添加了两个内容:

  1. 悬停时为 rightbackground-position
  2. background-position 的转换持续时间为 0

这意味着,在鼠标悬停时,我们会将 background-positionleft 过渡到 right,这样背景的大小就会从右边增加;然后,当鼠标离开时,transition 以相反的方式进行,从 rightleft,使它看起来像是我们正在从左侧减小背景的大小。这样我们的悬停效果就完成了!

!!但是,有没有发现我们的代码和最终的代码相比多了一行

没错,左右值分别可以修改为 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 定义了 backgroundpositionsize。悬停时,它会更新它们。这是一个非常好的 demo,展示了自定义属性如何帮助我们减少冗余代码并避免多次编写属性。

但是 Geoff 描述的效果却是相反的,从左到右。当我们不能依赖于同一个变量时,该怎么做呢?

我们仍然可以使用一个 var 变量,然后稍微更新代码以达到相反的效果,现在我们想要的从 100% 到 0% 而不是从 0% 到 100%,可以使用 calc() 表示的差值为 100%,如下所示:

.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%,但背景的位置将从 100% 改变到 0%,感谢 calc()

在我们进行下一个悬停效果之前,我想强调一些重要的东西,你可能已经注意到了。当处理自定义属性时,我使用 0%(带单位)而不是无单位的 0。当自定义属性单独使用时,无单位的 0 可能有效,但在 calc() 中会失效。想知道为什么会这样?StackOverflow 里有两个答案可以参考(答案一答案二)。

第二个 hover 效果

我们需要一个更复杂的过渡来实现这个效果。请看下面的分步说明:

1-2.png

最初,给 gradient 设置一个固定高度和全宽,然后我们向右移动渐变以覆盖底部。最后,我们将渐变的固定高度增加到 100%,以覆盖整个元素。

首先有一个背景位置过渡,然后是背景大小过渡。让我们将其转换为代码:

.hover-2 {
  background-image: linear-gradient(#1095c1 0 0);
  background-size: 100% .08em; /* .08em 是固定高度; 可根据需要设置. */
  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: /* ??? */;
}

请注意使用两个过渡值。悬停时,我们需要先更改位置,然后更改大小,因此我们要为 size 添加延迟。鼠标离开后,我们会做相反的事情。

现在的问题是:我们使用什么值作为背景位置?在上面我们留下了空白。background-size 值很小,但 background-position 的值还未设置。现在的配置,是出不来移动的渐变效果的。

我们的渐变宽度等于 100%,所以我们不能使用 background-position 的百分比值来移动它。

background-position 一起使用的百分比值总是令人痛苦,尤其是当你第一次使用它们时。它们的行为不直观,但定义明确,如果我们理解背后的逻辑,就很容易理解。需要 另一篇文章 来全面解释为什么它是这样工作的。建议花几分钟的时间了解一下,也可以查一下 MDN

诀窍:将宽度更改为与 100% 不同的值。此处我们使用 200%。不用担心背景超过元素,因为 overflowhidden 的。

.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%;
}

原文里的代码块,可以给某行更改的代码单独加背景色,就像 git 对比时的效果一样,呜呜呜。。。

用简写属性的方式优化代码

.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;
}

size 从 0.8em 到 100%,位置从 200% 到 100%;

我还使用另一个变量 --t 来优化过渡属性。鼠标悬停时,我们将其设置为 0.3s:

transition: .3s .3s, background-position .3s 0s;

鼠标离开时,--t 未定义,暂时设置为 3s:

transition: .3s 0s, background-position .3s .3s;

第三个 hover 效果

对于这种效果,我们将使用两个 gradient,而不是一个。你会发现,组合多个 gradient 是创建特殊悬停效果的另一种方式。

2.png

我们初始化两个 gradient,使其溢出不可见。每个元素都有一个固定的高度,各设为元素宽度的一半。然后让它们滑入视图,使其可见。第一个渐变的开始位于左下角,第二个渐变位于右上角。最后,我们增加高度以覆盖整个元素。

.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;
}

代码与我们讨论的其他悬停效果几乎相同,唯一的区别是我们有两个 gradient,有两个不同的位置,位置的值可能看起来很奇怪,但这又与 CSS 中百分比如何与 background-position 属性一起工作有关,因此,如果您想了解具体细节,我强烈建议您阅读 Stack Overflow 解答

优化代码:

.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% 作为背景大小,因为它可以防止 gradient 之间出现间隙。出于类似的原因,我还为 position 增加了 1%。

如下图,掘金中如果可以内嵌 CodePen 是多棒的体验

3.png

第四个 hover 效果

第四个效果是最难的,如果前面三个效果你已经掌握了,这对你来说将会是小菜一碟.

4.png

第一步,我们初始化两个零维的 gradient;第二步,增加它们的 size;第三步,不断增加它们的宽度,直到它们完全覆盖元素;然后,让它们滑到底部,更新其位置,这是悬停效果的 “魔法” 部分;由于两个渐变都将使用相同的颜色,因此在第四步中更改它们的位置不会产生视觉上的差异,但在第五步鼠标离开,减少 size 的时候将会看到差异。

.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%;
}

position 的值很明确,一个 gradient 是从 top left (0 0) 开始,结束于 bottom left (0 100%);另一个渐变从 top right (100% 0) 开始,结束于 bottom right (100% 100%)。

我们给 background-position 和 background-size 加上过渡效果,目前 background-size 还缺少一个过渡值,background-position 需要立即更改,因此给它设置的过渡时间为 0。

对于 size,两个 gradient 都需要具有 0 宽度和两倍元素高度(0% 200%)。稍后我们将看到它们的大小在悬停时是如何变化的。下图显示了每个渐变的配置:

5.webp

请注意,对于第二个渐变(绿色的),我们需要知道在创建的二次曲线渐变内使用它的高度。因此,我们给元素添加一个 line-height,然后给 conic-gradient 设置相同的值:

.hover-4 {
  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);
}

剩下的最后一件事就是计算出背景的大小。直觉上,我们可能认为每个 gradient 需要占据元素宽度的一半,但实际上这还不够。

7.webp

我们得到的间隙等于高度,所以我们实际上需要将每个 gradient 的大小增加悬停高度的一半,以便覆盖整个元素。

.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, #000 0) 
      0  var(--p, 0%) / var(--s, 0%) 200% no-repeat,
    conic-gradient(from -135deg at 1.2em 50%, #000 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 制作的悬停效果。这是在第四个效果上的升级版!但请注意,由于一个已知的 bug,它缺少 Firefox 支持。不过,将渐变与混合模式相结合以创建更酷的悬停效果仍然是一个很好的主意

在线代码预览

结束语

我们制作了四个超级酷的悬停效果!尽管它们是不同的效果,但它们都采用了相同的方法,CSS background 属性、自定义属性和 calc()。不同的组合允许我们制作不同的版本,所有版本都使用相同的技术,这些技术为我们留下了干净、可维护的代码。

如果你想得到一些想法,我收集了 500个(你没看错,500!)悬停效果,其中400个没有伪元素。我们在本文中讨论的四个问题只是冰山一角!