保留边框圆角的情况下扩展盒子

Avatar of Ana Tudor
Ana Tudor

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

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

Animated gif recording the CodePen expanding box effect on hover.
CodePen 主页上的扩展盒子效果。

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

Collage. On the left side, there is a DevTools screenshot showing the initial styles applied on the ::after pseudo-element. The relevant ones are those making it absolutely positioned with an offset of 1rem from the top and left and with an offset of -1rem from the right and bottom. On the right side, we have an illustration of these styles, showing the parent element box, the ::after box and the offsets between their edges.
初始 ::after 样式。正偏移量从父元素的 padding 限制向内移动,而负偏移量则向外移动。

:hover 上,它的偏移量被覆盖,并结合 transition,我们获得了扩展盒子效果。

Collage. On the left side, there is a DevTools screenshot showing the :hover styles applied on the ::after pseudo-element. These are all offsets overriding the initial ones and making the boundary of the ::after shift outwards by 2rem in all directions except the right. On the right side, we have an illustration of these styles, showing the parent element box, the ::after box and the offsets between their edges.
:hover 上的 ::after 样式。

right 属性在初始规则集和 :hover 规则集中都具有相同的值 (-1rem),因此无需覆盖它,但所有其他偏移量都向外移动 2rem(从 topleft 偏移量的 1rem-1rem,以及从 bottom 偏移量的 -1rem-3rem)。

需要注意的是,::after 伪元素具有 10pxborder-radius,在扩展时会保留该圆角。这让我想到,我们有哪些方法可以在保留 border-radius 的情况下扩展/收缩(伪)元素。你能想到多少种?如果你有任何想法没有包含在下面,请告诉我,我们将看看一些选项,并看看哪种选项最适合哪种情况。

更改偏移量

这是 CodePen 使用的方法,它在特定情况下非常有效,原因很多。首先,它具有良好的支持。当扩展的(伪)元素具有响应性时,它也起作用,没有固定尺寸,并且同时扩展的量是固定的(一个 rem 值)。它也适用于多个方向上的扩展(在本例中是 topbottomleft)。

但是,我们需要注意一些注意事项。

首先,扩展的元素不能具有 position: static。在 CodePen 的使用场景中这不是问题,因为 ::after 伪元素无论如何都需要绝对定位,以便放置在父元素内容的下方。

其次,过度使用偏移量动画(以及通常,使用盒子属性(如偏移量、边距、边框宽度、填充或尺寸)来动画任何影响布局的属性)会对性能产生负面影响。同样,这里也没有什么问题,我们只是在 :hover 上使用了一个小的 transition,没有大问题。

更改尺寸

我们可以更改尺寸而不是更改偏移量。但是,这种方法仅适用于我们希望(伪)元素最多在两个方向上扩展的情况。否则,我们需要更改偏移量。为了更好地理解这一点,让我们考虑 CodePen 的情况,其中我们希望 ::after 伪元素在三个方向上扩展(topbottomleft)。

相关的初始大小信息如下

.single-item::after {
  top: 1rem;
  right: -1rem;
  bottom: -1rem;
  left: 1rem;
}

由于相反的偏移量(top-bottomleft-right 对)相互抵消 (1rem - 1rem = 0),因此伪元素的尺寸等于其父元素的尺寸(或父元素尺寸的 100%)。

因此,我们可以将上述内容改写为

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: 100%;
  height: 100%;
}

:hover 上,我们将 width 向左增加 2rem,将 height 增加 4remtop 增加 2rembottom 增加 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 属性进行方向缩放。

更改填充/边框宽度

类似于更改尺寸,我们可以更改 paddingborder-width(对于 transparentborder)。请注意,与更改尺寸一样,如果要在多个方向上扩展盒子,还需要更新偏移量

查看 thebabydino 在 CodePen 上创建的 (@thebabydino)。

在上面的演示中,粉红色的盒子代表 ::after 伪元素的 content-box,你可以看到它保持相同的尺寸,这对这种方法来说很重要。

为了理解为什么它很重要,请考虑另一个限制:我们还需要通过两个偏移量加上 widthheight 来定义盒子尺寸,而不是使用所有四个偏移量。这是因为如果我们要使用四个偏移量而不是两个偏移量加上 widthheightpadding/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-sizepadding/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

但是,请注意,使用这种方法,我们需要使用缩放比例,而不是我们沿这个或那个方向扩展(伪)元素的量。从尺寸和扩展量获取缩放比例是可能的,但只有当它们以在它们之间具有特定固定关系的单位表示时。虽然预处理器可以混合使用像inpx这样的单位,因为1in始终为96px,但它们无法解析1em1%1vmin1chpx中的值,因为它们缺乏上下文。calc()也不是解决方案,因为它不允许我们将长度值除以另一个长度值以获得无单位的缩放比例。

这就是为什么缩放不是 CodePen 案例中的解决方案的原因,在该案例中,::after框的尺寸取决于视窗,同时又以固定的rem量进行扩展。

但是,如果我们的缩放量是给定的,或者我们可以轻松地计算它,这是一个需要考虑的选择,尤其是因为将缩放比例设为自定义属性,然后使用一些 Houdini 魔法对其进行动画处理,可以极大地简化我们的代码。

border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
transform: scale(var(--fx), var(--fy))

请注意,Houdini 仅在启用了**实验性 Web 平台功能**标志的 Chromium 浏览器中有效。

例如,我们可以创建这个瓷砖网格动画。

循环瓷砖网格动画 (演示,仅限带标志的 Chrome)

方形瓷砖的边长为$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})

另一个例子是下面这个,其中点是使用伪元素创建的。

循环尖刺动画 (演示,仅限带标志的 Chrome)

由于伪元素会与其父元素一起缩放,因此我们需要在其上反转缩放变换。

.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(),它的工作原理如下所示。

Illustration showing what the four values of the inset() function represent. The first one is the offset of the top edge of the clipping rectangle with respect to the top edge of the border-box. The second one is the offset of the right edge of the clipping rectangle with respect to the right edge of the border-box. The third one is the offset of the bottom edge of the clipping rectangle with respect to the bottom edge of the border-box. The fourth one is the offset of the left edge of the clipping rectangle with respect to the left edge of the border-box.
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范围的子元素时无法正常工作,就像前面讨论的缩放方法中给出的最后一个示例一样。