将 (伪) 元素限制在其父元素的边框盒内

Avatar of Ana Tudor
Ana Tudor

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

您是否曾经想过确保 (伪) 元素的任何内容都不会显示在其父元素的 border-box 之外?如果您不确定它是什么样子,假设我们想要以最少的标记和避免脆弱的 CSS 获得以下结果。

Screenshot of the result we want to get, highlighting the fact that even though an element has both a padding and a border, its descendant gets clipped to the limit of its border.
所需的结果。

这意味着我们不能仅仅为了视觉目的添加任何元素,也不能通过多个部分创建形状,无论是直接创建还是通过蒙版创建。 我们还希望避免在生成的代码中出现很长的、很长的任何列表(例如十几个 background 图层或方框阴影或 polygon() 函数内的点),因为虽然结果可能很有趣,但实际上无法做到这一点!

鉴于箭头指向的部分,您认为我们如何才能实现这一点? 在查看下面的解决方案之前,您想尝试一下吗? 乍一看它很简单,但一旦您真正尝试一下,您就会发现它要困难得多。

标记

每个项目都是一个段落 (<p>) 元素。 我很懒,我用 Pug 从一个对象数组中生成了它们,这些对象数组包含项目的渐变停止列表及其段落文本

- var data = [
-   {
-     slist: ['#ebac79', '#d65b56'], 
-     ptext: 'Pancake muffin chocolate syrup brownie.'
-   }, 
-   {
-     slist: ['#90cbb7', '#2fb1a9'], 
-     ptext: 'Cake lemon berry muffin plum macaron.'
-   }, 
-   {
-     slist: ['#8a7876', '#32201c'], 
-     ptext: 'Wafer apple tart pie muffin gingerbread.'
-   }, 
-   {
-     slist: ['#a6c869', '#37a65a'], 
-     ptext: 'Liquorice plum topping chocolate lemon.'
-   }
- ].reverse();
- var n = data.length;

while n--
  p(style=`--slist: ${data[n].slist}`) #{data[n].ptext}

会生成 以下平淡无奇的 HTML

<p style='--slist: #ebac79, #d65b56'>Pancake muffin chocolate syrup brownie.</p>
<p style='--slist: #90cbb7, #2fb1a9'>Cake lemon berry muffin plum macaron.</p>
<p style='--slist: #8a7876, #32201c'>Wafer apple tart pie muffin gingerbread.</p>
<p style='--slist: #a6c869, #37a65a'>Liquorice plum topping chocolate lemon.</p>

基本样式

对于段落元素,我们设置了一个 font、尺寸和一个等于 height 值一半的 border-radius

$w: 26em;
$h: 5em;

p {
  width: $w; height: $h;
  border-radius: .5*$h;
  background: silver;
  font: 1.5em/ 1.375 trebuchet ms, verdana, sans-serif;
}

我们还设置了一个虚拟 background,这样我们就可以看到它们的限制

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

段落背景

我们有三个从上到下的渐变,这意味着我们可以将它们中的每一个放在不同的布局框的限制内:顶部渐变层限制在 content-box 内,中间层限制在 padding-box 内,底部层限制在 border-box 内。如果您需要深入了解此技术的复习,请查看 这篇文章,但基本思想是您将这些布局框视为嵌套的矩形。

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
布局框。(演示

这几乎是浏览器 DevTools 如何呈现它们的。

Screenshot collage showing the graphical representation of the layout boxes in browsers' DevTools.
Chrome(左)、Firefox(中)和 Edge(右)显示的布局框。

您可能想知道为什么我们不会通过 background-size 给定的不同大小的渐变进行分层,这些渐变具有 background-repeat: no-repeat。 嗯,这是因为我们只能获得没有圆角的矩形。

使用 background-clip 方法,如果我们有一个 border-radius,我们的 background 图层将遵循它。 同时,我们设置的实际 border-radius 用于对 border-box 的角进行圆角处理;相同半径减去 border-widthpadding-box 的角进行圆角处理。 然后我们再减去 padding 来对 content-box 的角进行圆角处理。

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

那么让我们来编码吧!

我们设置一个 transparent border 和一个 padding。 我们通过切换到 box-sizing: border-box 来确保它们从我们设置的尺寸中减去。 最后,我们分层三个渐变:顶部渐变限制在 content-box 内,中间渐变限制在 padding-box 内,底部渐变限制在 border-box 内。

p {
  /* same styles as before */
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  border: solid $b transparent;
  padding: $p;
  background: 
    linear-gradient(#dbdbdb, #fff) content-box, 
    linear-gradient(var(--slist)) padding-box, 
    linear-gradient(#fff, #dcdcdc) border-box;
  text-indent: 1em;
}

我们还设置了一个 flex 布局和一个 text-indent 来将文本内容从横幅边缘移开

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

编号

在我们继续进行棘手的部分之前,让我们先把段落编号处理掉!

我们使用 counter 将它们添加为 :after 伪元素上的 content 值。 我们首先使这个 :after 成为一个正方形,其边长等于段落 height(即 $h)减去顶部和底部的 border-width(均等于 $b)。 然后我们通过在上面设置 border-radius: 50% 将此正方形变成圆形。 我们使其 inherit 其父元素的 box-sizingborder,然后以类似于其父元素的方式设置其 background

$d: $h - 2*$b;

p {
  /* same styles as before */
  counter-increment: c;

  &:after {
    box-sizing: inherit;
    border: inherit;
    width: $d; height: $d;
    border-radius: 50%;
    box-shadow: 
      inset 0 0 1px 1px #efefef, 
      inset 0 #{-$b} rgba(#000, .1);
    background: 
      linear-gradient(var(--slist)) padding-box, 
      linear-gradient(#d0d0d0, #e7e7e7) border-box;
    color: #fff;
    content: counter(c, decimal-leading-zero);
  }
}

好吧,这开始看起来像样了!

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

我们还需要对这个 :after 伪元素的 CSS 进行一些调整——一个等于其父元素填充值的 margin-right 以及对其内部布局的调整,以便将数字放在正中间。 编号部分差不多就是这样了!

p {
  /* same styles as before */

  &:after {
    /* same styles as before */
    display: grid;
    place-content: center;
    margin-right: -$p;
    text-indent: 0;
  }
}

我们越来越接近了!

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

棘手的部分

我们终于到了这里!

我们首先使用 :before 伪元素,将其绝对定位在 right 侧,并使其成为一个边长等于其父元素 heightsquare

p {
  /* same styles as before */
  position: relative;
  outline: solid 2px orange;

  &:before {
    position: absolute;
    right: -$b;
    width: $h;
    height: $h;
    outline: solid 2px purple;
    content: '';
  }
}

我们还给这个伪元素及其父元素设置了一些虚拟轮廓,这样我们就可以检查对齐方式

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

好了,现在我们给这个 :before 设置一个虚拟 background,旋转它,然后给它设置一个 border-radius 和一个漂亮的 box-shadow

p {
  /* same styles as before */

  &:before {
    /* same styles as before */
    border-radius: $b;
    transform: rotate(45deg);
    box-shadow: 0 0 7px rgba(#000, .2);
    background: linear-gradient(-45deg, orange, purple);
  }
}

我们得到以下结果!

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

现在我们有一个小问题::before 伪元素是绝对定位的,现在位于包含编号的 :after 伪元素的顶部! 我们可以通过在 :after 伪元素上设置 position: relative 来解决这个问题。

查看 thebabydino 的 Pen (@thebabydino) 在 CodePen 上。

事情开始变得有趣了!

限制背景渐变

首先,我们需要在 :before 伪元素的渐变上设置停止位置,以使它们与父元素的 bottomtop 边缘相匹配。 这是因为我们希望在父元素的 top 边缘有特定的十六进制值,在父元素的 bottom 边缘有特定的十六进制值。

Annotated illustration. Shows the parent paragraph and its rotated :before pseudo-element, the gradient direction and the stop lines at the positions we're looking for.
我们需要计算的停止位置。

由于我们已将正方形 :before 旋转了 45°,因此其左上角现在向上指向(反之,其右下角向下指向)。

Animated .gif. Shows the square :before positioned on the right of its parent. Its top left corner and bottom right corner are highlighted as well as its vertical axis (vertical line passing through the intersection of its diagonals). Rotating our square by 45° means its top left corner now points up (and its bottom right corner points down).
旋转如何改变正方形角的位置。

指向正方形左上角的渐变是 -45° 方向的渐变(因为 角在 12 点钟位置,正方向与变换一样,是顺时针方向)。 指向角的渐变意味着 100% 点位于该角)

Animated .gif. Shows the square :before positioned on the right of its parent. Shows the linear gradient to the top left corner. After the rotation, since the top left corner points up, the gradient direction also goes up.
旋转如何改变渐变方向。

渐变的 50% 线始终穿过渐变框的中点(对角线相交的点)。

渐变框是在其中绘制渐变的框,其大小由 background-size 给定。 由于我们没有设置 background-size,因此渐变的默认设置是使用由 background-origin 定义的整个框,默认情况下为 padding-box。 由于我们的 :before 伪元素没有 borderpadding,因此所有三个框(content-boxpadding-boxborder-box)在它们之间具有相等的间距,并且与渐变框的比例相同。

在我们的例子中,我们有以下垂直于 -45° 指向渐变线的线的

Annotated illustration. Shows the parent paragraph and its rotated :before pseudo-element, the gradient direction and the stop lines at 0% and 100%, at the two positions we want to get and at 50%.
获得相关的停止位置。
  • 0% 线,穿过 :before 的右下角
  • 伪元素的段落父元素的 bottom 边缘
  • 将正方形对角线分割成两个镜像的等腰直角三角形的 50% 线;根据段落及其伪元素的对齐方式,这条线也是段落本身的中心线,将其分成两半,每半的 height 等于段落 height 的一半($h)。
  • 伪元素段落父元素的 top 边缘
  • 100% 线,穿过 :before 的左上角

这意味着我们需要将 :before 伪元素上的 -45° 指向的渐变限制在 calc(50% - #{.5*$h})(对应段落的 bottom 边缘)和 calc(50% + #{.5*$h})(对应段落的 top 边缘)之间。

当然,这确实可以做到!

linear-gradient(-45deg, orange calc(50% - #{.5*$h}), purple calc(50% + #{.5*$h}))

请参阅 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

在这些停止位置添加从透明到透明的急剧过渡,可以更明显地看出它们是正确的。

linear-gradient(-45deg, 
      transparent calc(50% - #{.5*$h}), orange 0, 
      purple calc(50% + #{.5*$h}), transparent 0)

请参阅 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

限制伪元素本身

下一步是防止 :before 伪元素溢出其父元素的边界。

很简单吧?只需在段落上设置 overflow: hidden 就行了!

那就这么做吧!

这是我们得到的结果

请参阅 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

糟糕,这不是我们想要的结果!

Side by side comparison of what we have using overflow: hidden (:before gets clipped to its parent's padding-box) and what we actually want to get (:before gets clipped to its parent's border-box).
我们用 overflow: hidden(左)得到的结果与我们想要的结果(右)对比。

问题是 overflow: hidden 会将元素 padding-box 外部的所有内容都裁剪掉,而我们这里想要的是裁剪 :before 伪元素在 border-box 外部的部分,而 border-box 在我们的例子中比 padding-box 大,因为我们有一个非零的 border,我们无法将其丢弃(并通过使 border-box 等于 padding-box 来解决问题),因为我们需要在段落上设置三个 background 层:最上面一层覆盖 content-box,中间一层覆盖 padding-box,最下面一层覆盖 border-box

解决办法?如果你看了一下标签,你可能已经猜到了:使用 clip-path 代替!

几乎所有使用 clip-path 的文章和演示都使用 SVG 引用或 polygon() 形状函数,但这些并不是我们唯一的选择!

另一个可能的形状函数(也是我们在这里将要使用的)是 inset()。此函数指定一个裁剪矩形,该矩形由距 toprightbottomleft 边缘的距离定义。什么边缘?默认情况下1,这是 border-box 的边缘,这正是我们这里需要的!

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() 函数的工作原理。(Demo)

所以让我们丢弃 overflow: hidden,改用 clip-path: inset(0)。这是我们得到的结果

请参阅 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

这比以前好,但还不是我们想要的,因为它没有考虑到段落的 border-radius。幸运的是,inset() 也让我们可以指定一个圆角,它可以接受我们想要的任何 border-radius 值。没错,任何有效的 border-radius 值都可以使用——例如,这个

clip-path: inset(0 round 15% 75px 35vh 13vw/ 3em 5rem 29vmin 12.5vmax)

不过我们只需要更简单的东西

$r: .5*$h;

p {
  /* same styles as before */
  border-radius: $r;
  clip-path: inset(0 round $r)
}

现在我们终于得到了我们想要的结果

请参阅 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。

最后的润色

由于我们不希望 :before 上出现紫色-橙色的渐变,因此我们将它们替换为我们需要的实际值。然后我们将段落放置在中间,因为这样看起来更好。最后,我们通过在 body 上设置 drop-shadow() 来为段落添加阴影(我们不能在段落本身使用 box-shadow,因为我们使用了 clip-path,它会裁剪掉 box-shadow,所以我们看不到它)。就是这样!

请参阅 thebabydino 在 CodePen 上创建的 Pen (@thebabydino)。


  1. 我们应该能够更改这个 <geometry-box> 值,但 Chrome 没有实现规范的这部分内容。有一个 问题 为此打开,你可以对此问题加星标,或者在其中留下评论,说明你更改默认值的用例。