我最近注意到 CodePen 上的一个有趣变化:将鼠标悬停在主页上的笔上时,背景会扩展一个具有圆角的矩形。

出于好奇,我不得不检查一下它是如何工作的!事实证明,背景中的矩形是一个绝对定位的 ::after
伪元素。

::after
样式。正偏移量从父元素的 padding
限制向内移动,而负偏移量则向外移动。在 :hover
上,它的偏移量被覆盖,并结合 transition
,我们获得了扩展盒子效果。

:hover
上的 ::after
样式。right
属性在初始规则集和 :hover
规则集中都具有相同的值 (-1rem
),因此无需覆盖它,但所有其他偏移量都向外移动 2rem
(从 top
和 left
偏移量的 1rem
到 -1rem
,以及从 bottom
偏移量的 -1rem
到 -3rem
)。
需要注意的是,::after
伪元素具有 10px
的 border-radius
,在扩展时会保留该圆角。这让我想到,我们有哪些方法可以在保留 border-radius
的情况下扩展/收缩(伪)元素。你能想到多少种?如果你有任何想法没有包含在下面,请告诉我,我们将看看一些选项,并看看哪种选项最适合哪种情况。
更改偏移量
这是 CodePen 使用的方法,它在特定情况下非常有效,原因很多。首先,它具有良好的支持。当扩展的(伪)元素具有响应性时,它也起作用,没有固定尺寸,并且同时扩展的量是固定的(一个 rem
值)。它也适用于多个方向上的扩展(在本例中是 top
、bottom
和 left
)。
但是,我们需要注意一些注意事项。
首先,扩展的元素不能具有 position: static
。在 CodePen 的使用场景中这不是问题,因为 ::after
伪元素无论如何都需要绝对定位,以便放置在父元素内容的下方。
其次,过度使用偏移量动画(以及通常,使用盒子属性(如偏移量、边距、边框宽度、填充或尺寸)来动画任何影响布局的属性)会对性能产生负面影响。同样,这里也没有什么问题,我们只是在 :hover
上使用了一个小的 transition
,没有大问题。
更改尺寸
我们可以更改尺寸而不是更改偏移量。但是,这种方法仅适用于我们希望(伪)元素最多在两个方向上扩展的情况。否则,我们需要更改偏移量。为了更好地理解这一点,让我们考虑 CodePen 的情况,其中我们希望 ::after
伪元素在三个方向上扩展(top
、bottom
和 left
)。
相关的初始大小信息如下
.single-item::after {
top: 1rem;
right: -1rem;
bottom: -1rem;
left: 1rem;
}
由于相反的偏移量(top
-bottom
和 left
-right
对)相互抵消 (1rem - 1rem = 0
),因此伪元素的尺寸等于其父元素的尺寸(或父元素尺寸的 100%
)。
因此,我们可以将上述内容改写为
.single-item::after {
top: 1rem;
right: -1rem;
width: 100%;
height: 100%;
}
在 :hover
上,我们将 width
向左增加 2rem
,将 height
增加 4rem
,top
增加 2rem
,bottom
增加 2rem
。但是,仅仅写
.single-item::after {
width: calc(100% + 2rem);
height: calc(100% + 4rem);
}
…是不够的,因为这会导致 height
向下方向增加 4rem
,而不是向上增加 2rem
,向下增加 2rem
。下面的演示说明了这一点(将 :focus
放置在项目上或将鼠标悬停在项目上,以查看 ::after
伪元素如何扩展)
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
我们需要更新 top
属性才能获得所需的效果
.single-item::after {
top: -1rem;
width: calc(100% + 2rem);
height: calc(100% + 4rem);
}
这有效,如下所示
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
但是,老实说,这比单独更改偏移量感觉不那么理想。
但是,更改尺寸在不同类型的场景中是一个很好的解决方案,例如当我们希望一些具有圆角的条形在单个方向上扩展/收缩时。
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
请注意,如果我们没有要保留的圆角,更好的解决方案是使用 transform
属性进行方向缩放。
更改填充/边框宽度
类似于更改尺寸,我们可以更改 padding
或 border-width
(对于 transparent
的 border
)。请注意,与更改尺寸一样,如果要在多个方向上扩展盒子,还需要更新偏移量
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
在上面的演示中,粉红色的盒子代表 ::after
伪元素的 content-box
,你可以看到它保持相同的尺寸,这对这种方法来说很重要。
为了理解为什么它很重要,请考虑另一个限制:我们还需要通过两个偏移量加上 width
和 height
来定义盒子尺寸,而不是使用所有四个偏移量。这是因为如果我们要使用四个偏移量而不是两个偏移量加上 width
和 height
,padding
/border-width
只会向内增长。
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
出于同样的原因,我们不能在 ::after
伪元素上使用 box-sizing: border-box
。
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
尽管存在这些限制,但如果扩展的(伪)元素具有我们不想在 :hover
上看到移动的文本内容,这种方法会很方便,如下面的笔所示,其中前两个示例更改了偏移量/尺寸,而最后两个示例更改了填充/边框宽度
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
更改边距
使用这种方法,我们首先将偏移量设置为 :hover
状态值,并使用 margin
进行补偿,以提供初始状态大小
.single-item::after {
top: -1rem;
right: -1rem;
bottom: -3rem;
left: -1rem;
margin: 2rem 0 2rem 2rem;
}
然后,我们在 :hover
上将该 margin
设置为零
.single-item:hover::after { margin: 0 }
查看 thebabydino 在 CodePen 上创建的 笔 (@thebabydino)。
这是另一种非常适合 CodePen 场景的方法,但我无法想到其他用例。还要注意,与更改偏移量或尺寸一样,这种方法会影响 content-box
的大小,因此我们可能拥有的任何文本内容都会被移动和重新排列。
更改字体大小
这可能是最棘手的一种,并且有许多限制,其中最重要的是我们不能在实际扩展/收缩的(伪)元素上包含文本内容——但这在 CodePen 场景中仍然是一个可行的方法。
另外,font-size
本身并不能真正做任何事情来使盒子扩展或收缩。我们需要将它与之前讨论的某个属性结合起来使用。
例如,我们可以将::after
上的font-size
设置为等于1rem
,将偏移量设置为扩展后的情况,并设置与扩展状态和初始状态之间的差异相对应的em
边距。
.single-item::after {
top: -1rem;
right: -1rem;
bottom: -3rem;
left: -1rem;
margin: 2em 0 2em 2em;
font-size: 1rem;
}
然后,在:hover
上,我们将font-size
调整为0
。
.single-item:hover::after { font-size: 0 }
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
我们也可以将font-size
与偏移量一起使用,不过会稍微复杂一些。
.single-item::after {
top: calc(2em - 1rem);
right: -1rem;
bottom: calc(2em - 3rem);
left: calc(2em - 1rem);
font-size: 1rem;
}
.single-item:hover::after { font-size: 0 }
尽管如此,重要的是它可以正常工作,如下所示。
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
将font-size
与尺寸结合起来更麻烦,因为我们还需要在:hover
上更改垂直偏移量,并将其置于所有内容之上。
.single-item::after {
top: 1rem;
right: -1rem;
width: calc(100% + 2em);
height: calc(100% + 4em);
font-size: 0;
}
.single-item:hover::after {
top: -1rem;
font-size: 1rem
}
好吧,至少它可以正常工作。
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
使用font-size
与padding
/border-width
结合也是如此。
.single-item::after {
top: 1rem;
right: -1rem;
width: 100%;
height: 100%;
font-size: 0;
}
.single-item:nth-child(1)::after {
padding: 2em 0 2em 2em;
}
.single-item:nth-child(2)::after {
border: solid 0 transparent;
border-width: 2em 0 2em 2em;
}
.single-item:hover::after {
top: -1rem;
font-size: 1rem;
}
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
更改缩放比例
如果您已经阅读过有关animation
性能的文章,那么您可能已经阅读过,最好对变换进行动画处理,而不是对影响布局的属性进行动画处理,例如偏移量、边距、边框、填充、尺寸——基本上是我们目前使用的所有属性!
这里突出的第一个问题是,缩放元素也会缩放其圆角,如下所示。
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
我们可以通过以相反的方式缩放border-radius
来解决这个问题。
假设我们沿x轴按$fx
的比例缩放元素,沿y轴按$fy
的比例缩放元素,并且我们希望保持其border-radius
为一个常数$r
。
这意味着我们还需要将$r
除以每个轴的相应缩放比例。
border-radius: #{$r/$fx}/ #{$r/$fy};
transform: scale($fx, $fy)
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
但是,请注意,使用这种方法,我们需要使用缩放比例,而不是我们沿这个或那个方向扩展(伪)元素的量。从尺寸和扩展量获取缩放比例是可能的,但只有当它们以在它们之间具有特定固定关系的单位表示时。虽然预处理器可以混合使用像in
或px
这样的单位,因为1in
始终为96px
,但它们无法解析1em
或1%
或1vmin
或1ch
在px
中的值,因为它们缺乏上下文。calc()
也不是解决方案,因为它不允许我们将长度值除以另一个长度值以获得无单位的缩放比例。
这就是为什么缩放不是 CodePen 案例中的解决方案的原因,在该案例中,::after
框的尺寸取决于视窗,同时又以固定的rem
量进行扩展。
但是,如果我们的缩放量是给定的,或者我们可以轻松地计算它,这是一个需要考虑的选择,尤其是因为将缩放比例设为自定义属性,然后使用一些 Houdini 魔法对其进行动画处理,可以极大地简化我们的代码。
border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
transform: scale(var(--fx), var(--fy))
请注意,Houdini 仅在启用了**实验性 Web 平台功能**标志的 Chromium 浏览器中有效。
例如,我们可以创建这个瓷砖网格动画。
方形瓷砖的边长为$l
,圆角为$k*$l
。
.tile {
width: $l;
height: $l;
border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
transform: scale(var(--fx), var(--fy))
}
我们注册了两个自定义属性。
CSS.registerProperty({
name: '--fx',
syntax: '<number>',
initialValue: 1,
inherits: false
});
CSS.registerProperty({
name: '--fy',
syntax: '<number>',
initialValue: 1,
inherits: false
});
然后我们可以对其进行动画处理。
.tile {
/* same as before */
animation: a $t infinite ease-in alternate;
animation-name: fx, fy;
}
@keyframes fx {
0%, 35% { --fx: 1 }
50%, 100% { --fx: #{2*$k} }
}
@keyframes fy {
0%, 35% { --fy: 1 }
50%, 100% { --fy: #{2*$k} }
}
最后,我们根据水平(--i
)和垂直(--j
)网格索引添加一个延迟,以创建交错的animation
效果。
animation-delay:
calc((var(--i) + var(--m) - var(--j))*#{$t}/(2*var(--m)) - #{$t}),
calc((var(--i) + var(--m) - var(--j))*#{$t}/(2*var(--m)) - #{1.5*$t})
另一个例子是下面这个,其中点是使用伪元素创建的。
由于伪元素会与其父元素一起缩放,因此我们需要在其上反转缩放变换。
.spike {
/* other spike styles */
transform: var(--position) scalex(var(--fx));
&::before, &::after {
/* other pseudo styles */
transform: scalex(calc(1/var(--fx)));
}
}
更改…剪切路径?!
这是一种我非常喜欢的方法,尽管它排除了预 Chromium Edge 和 Internet Explorer 的支持。
几乎所有clip-path
的使用示例都具有polygon()
值或 SVG 引用值。但是,如果您已经阅读过我之前的一些文章,那么您可能知道我们还可以使用其他基本形状,例如inset()
,它的工作原理如下所示。
inset()
函数的工作原理。(演示)因此,为了使用这种方法重现 CodePen 效果,我们将::after
偏移量设置为扩展状态值,然后使用clip-path
剪掉我们不想看到的部分。
.single-item::after {
top: -1rem;
right: -1rem;
bottom: -3em;
left: -1em;
clip-path: inset(2rem 0 2rem 2rem)
}
然后,在:hover
状态下,我们将所有内边距设置为零。
.single-item:hover::after {
clip-path: inset(0)
}
这可以在下面的实际操作中看到。
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
好吧,这可以正常工作,但我们还需要圆角。幸运的是,inset()
允许我们指定它,只要是我们希望的任何border-radius
值。
这里,对所有角落沿两个方向设置10px
。
.single-item::after {
/* same styles as before */
clip-path: inset(2rem 0 2rem 2rem round 10px)
}
.single-item:hover::after {
clip-path: inset(0 round 10px)
}
这正是我们想要的效果。
请参阅thebabydino (@thebabydino) 在 CodePen 上创建的 Pen。
此外,它在不支持的浏览器中不会真正破坏任何东西,它只是始终保持在扩展状态。
但是,虽然这种方法对于许多情况(包括 CodePen 使用案例)非常有效,但它在扩展/收缩元素具有超出其剪切父元素的border-box
范围的子元素时无法正常工作,就像前面讨论的缩放方法中给出的最后一个示例一样。
在您关于更改缩放比例的部分,您说:“….
calc()
也不是解决方案,因为它不允许我们将长度值除以另一个长度值以获得无单位的缩放比例。“。我不确定为什么我们不能做这样的事情。
它似乎可以正常工作:代码示例
我是不是错过了什么?
您引用的部分说
在您的示例中,您使用了给定的缩放比例,并除以它们以获得
border-radius
。这很好,我在示例中也有一个执行此操作的示例。但这与我在该引文中所说的无关。您并没有通过将扩展状态(即初始状态的尺寸加上扩展量)的尺寸除以初始状态的尺寸来计算缩放比例。例如,如果您初始尺寸为
30vw x 50vh
,并且您将框沿每个方向扩展了1rem
,那么缩放比例分别为水平方向上的(30vw + 2rem)/30vw
和垂直方向上的(50vh + 2rem)/50vh
。Sass 无法做到这一点,因为您在
rem
和视窗单位之间没有固定关系。calc()
也无法做到这一点,因为它不允许将长度值除以另一个长度值,这正是我在该引文中所说的。Ana,感谢您撰写这篇文章并分享了如此多的选项,但剪切方法非常棒。:)