CSS 中的模糊边框

Avatar of Ana Tudor
Ana Tudor 发布

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

假设我们想定位一个元素,并且只视觉上模糊它的边框。我们无法直接使用任何简单的内置 Web 平台功能。但是,我们可以使用一些 CSS 小技巧来实现它。

这是我们想要的效果

Screenshot of an element with a background image that shows oranges on a wooden table. The border of this element is blurred.
预期效果。

让我们看看如何编写代码来实现这种效果,如何使用圆角来增强它,如何扩展支持以使其跨浏览器工作,未来在这个领域会带来什么以及我们可以从相同的想法开始获得哪些其他有趣的结果!

编写基本的模糊边框代码

我们从一个元素开始,在这个元素上,我们设置了一些虚拟尺寸、一个部分透明(仅略微可见)的边框和一个背景,其大小相对于边框框,但其可见性限制在填充框

$b: 1.5em; // border-width

div {
  border: solid $b rgba(#000, .2);
  height: 50vmin;
  max-width: 13em;
  max-height: 7em;
  background: url(oranges.jpg) 50%/ cover 
                border-box /* background-origin */
                padding-box /* background-clip */;
}

background-origin指定的框是其左上角为background-position0 0点的框,也是background-size(在我们这里设置为cover)相对于的框。由background-clip指定的框是背景可见的框的边界。

background-origin的初始值为padding-boxbackground-clip的初始值为border-box,因此在这种情况下我们需要同时指定它们。

如果您需要更深入地了解background-originbackground-clip,您可以查看这篇关于该主题的详细文章

上面的代码会给我们以下结果

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

接下来,我们添加一个绝对定位的伪元素,它覆盖其父元素的整个边框框,并位于其后面(z-index: -1)。我们还使这个伪元素继承其父元素的边框背景,然后我们将边框颜色更改为透明,并将背景剪裁更改为边框框

$b: 1.5em; // border-width

div {
  position: relative;
  /* same styles as before */
  
  &:before {
    position: absolute;
    z-index: -1;
    /* go outside padding-box by 
     * a border-width ($b) in every direction */
    top: -$b; right: -$b; bottom: -$b; left: -$b;
    border: inherit;
    border-color: transparent;
    background: inherit;
    background-clip: border-box;
    content: ''
  }
}

现在我们也可以看到几乎不可见的边框后面的背景

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

好吧,您可能已经看到了事情的发展方向!下一步是对伪元素进行blur()处理。由于这个伪元素只在部分透明的边框下方可见(其余部分被其父元素的填充框限制的背景覆盖),因此会导致边框区域是图像中我们看到的唯一模糊区域。

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

我们还将元素的边框颜色的 alpha 值降低到.03,因为我们希望模糊度能够完成大部分突出显示边框位置的工作。

这可能看起来已经完成了,但我仍然不喜欢一件事:伪元素的边缘现在也被模糊了。所以让我们修复它!

在浏览器应用属性的顺序方面,一个方便的事情是过滤器在剪裁之前应用。虽然这不是我们想要的,并且迫使我们在很多其他情况下采用不方便的解决方法……就在这里,它被证明非常有用!

这意味着,在模糊伪元素之后,我们可以将其剪裁到其边框框

我首选的方法是将clip-path设置为inset(0),因为……这确实是实现它的最简单方法!polygon(0 0, 100% 0, 100% 100%, 0 100%)会太过复杂。

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

如果您想知道为什么不在实际元素上设置clip-path,而是在:before伪元素上设置它,这是因为在元素上设置clip-path会使它成为一个堆叠上下文。这会迫使所有子元素(以及因此也包括其模糊的:before伪元素)包含在其中,因此位于其背景的前面。然后,任何核武器z-index!important都无法改变这一点。

我们可以通过添加一些带有更漂亮字体盒子阴影和一些布局属性的文本来美化它。

如果我们有圆角呢?

使用inset()而不是polygon()作为clip-path的最佳之处在于,inset()还可以适应我们可能需要的任何border-radius

当我说是任何border-radius时,我是认真的!看看这个!

div {
  --r: 15% 75px 35vh 13vw/ 3em 5rem 29vmin 12.5vmax;
  border-radius: var(--r);
  /* same styles as before */
  
  &:before {
    /* same styles as before */
    border-radius: inherit;
    clip-path: inset(0 round var(--r));
  }
}

它运行得很好!

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

扩展支持

一些移动浏览器仍然需要-webkit-前缀才能使用filterclip-path,因此请确保也包含这些版本。请注意,它们包含在本文中嵌入的 CodePen 演示中,即使我在本文正文中提供的代码中选择跳过它们。

好吧,但如果我们需要支持 Edge 呢?clip-path在 Edge 中不起作用,但filter起作用,这意味着我们确实会得到模糊的边框,但没有清晰的剪裁限制。

嗯,如果我们不需要圆角,我们可以使用已弃用的clip属性作为后备。这意味着在clip-path之前添加以下行

clip: rect(0 100% 100% 0)

然后我们的演示现在在 Edge 中运行……有点!右侧、底部和左侧边缘被清晰地剪裁,但顶部的边缘仍然保持模糊(仅在 Pen 的调试模式下,在编辑器视图中的 iframe 中看起来一切正常)。打开开发者工具或在 Edge 窗口中右键单击或单击窗口外部的任何位置都会使该属性的效果消失。这是本月的错误!

好吧,由于它非常不可靠,即使我们想要圆角它也无济于事,所以让我们尝试另一种方法!

这有点像用右脚挠左耳(或者反过来,取决于哪一侧更灵活),但这是我能想到的让它在 Edge 中工作的唯一方法。

你们中的一些人可能已经在屏幕上大喊大叫:“但是 Ana……overflow: hidden!”是的,这就是我们现在要做的。我最初避免使用它是因为它的工作方式:它会剪裁掉填充框外部的所有后代内容。而不是边框框外部,就像我们通过剪裁所做的那样!

这意味着我们需要丢弃真正的边框,并用填充来模拟它,我对这并不完全满意,因为它会导致更多复杂情况,但让我们一步一步来!

就代码更改而言,我们首先要做的是删除所有与边框相关的属性,并将边框宽度值设置为填充。然后,我们设置overflow: hidden,并将实际元素的背景限制在内容框内。最后,我们将伪元素的背景剪裁重置为填充框值,并将它的偏移量归零。

$fake-b: 1.5em; // fake border-width

div {
  /* same styles as before */
  overflow: hidden;
  padding: $fake-b;
  background: url(oranges.jpg) 50%/ cover 
                padding-box /* background-origin */
                content-box /* background-clip */;
  
  &:before {
    /* same styles as before */
    top: 0; right: 0; bottom: 0; left: 0;
    background: inherit;
    background-clip: padding-box;
  }
}

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

如果我们想要那个几乎不可见的“边框”覆盖,我们需要在实际元素上再添加一个背景

$fake-b: 1.5em; // fake border-width
$c: rgba(#000, .03);

div {
  /* same styles as before */
  overflow: hidden;
  padding: $fake-b;
  --img: url(oranges.jpg) 50%/ cover;
  background: var(--img)
                padding-box /* background-origin */
                content-box /* background-clip */,  
              linear-gradient($c, $c);
  
  &:before {
    /* same styles as before */
    top: 0; right: 0; bottom: 0; left: 0;
    background: var(--img);
  }
}

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

我们也可以轻松地添加圆角

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

那为什么我们一开始不这样做呢?!

还记得我之前说过,不使用实际的边框可能会在以后使事情变得复杂吗?

好吧,假设我们想要添加一些文本。使用第一种方法,使用实际的边框clip-path,要防止文本内容接触模糊的边框,只需要在我们的元素上添加填充(例如1em)。

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

但是,对于overflow: hidden方法,我们已经使用padding属性来创建模糊的“边框”。增加它的值没有帮助,因为它只会增加伪边框的宽度。

我们可以将文本添加到子元素中。或者我们也可以使用:after伪元素!

这种方法的工作方式与第一种方法非常相似,:after 替换了实际的元素。不同的是,我们使用 overflow: hidden 而不是 clip-path: inset(0) 来裁剪模糊的边缘,而实际元素的 padding 是伪元素的 border-width ($b) 加上我们想要的任何 padding 值。

$b: 1.5em; // border-width

div {
  overflow: hidden;
  position: relative;
  padding: calc(1em + #{$b});
  /* prettifying styles */
	
  &:before, &:after {
    position: absolute;
    z-index: -1; /* put them *behind* parent */
    /* zero all offsets */
    top: 0; right: 0; bottom: 0; left: 0;
    border: solid $b rgba(#000, .03);
    background: url(oranges.jpg) 50%/ cover 
                  border-box /* background-origin */
                  padding-box /* background-clip */;
    content: ''
  }
	
  &:before {
    border-color: transparent;
    background-clip: border-box;
    filter: blur(9px);
  }
}

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

如何同时拥有文本和非常圆润的角?我们将在另一篇文章中讨论这个问题,敬请期待!

backdrop-filter 呢?

你们中的一些人可能想知道(就像我最初尝试实现这种效果时一样)backdrop-filter 是否不是一种选择。

既是也不是!

从技术上讲,可以实现相同的效果,但由于 Firefox 尚未实现它,如果我们选择这种方式,将放弃 Firefox 支持。更不用说这种方法还迫使我们使用两个伪元素才能获得对元素有文本内容情况的最佳支持(这意味着我们需要伪元素及其 padding-box 区域的 background 在此文本下方显示)。

更新:由于 回归backdrop-filter 技术不再适用于 Chrome,因此支持现在最好限制在 Safari 和 Edge。

对于那些还不知道 backdrop-filter 是做什么的:它会过滤掉我们对其应用的元素的(部分)transparent 部分可以看到的内容。

我们需要这样处理:两个伪元素都有一个透明的 border 和一个相对于 padding-box 定位和大小的 background。我们将顶部伪元素(:after)的 background 限制在 padding-box 范围内。

现在 :afterborder 区域不再有 background,我们可以透过它看到它后面的 :before 伪元素。我们在 :after 上设置 backdrop-filter,甚至可以将 border-colortransparent 更改为略微可见的颜色。底部(:before)伪元素的 background 仍然可以通过(部分)transparent、几乎不可区分的 :after 上部的 border 看见,由于应用了 backdrop-filter 而变得模糊。

$b: 1.5em; // border-width

div {
  overflow: hidden;
  position: relative;
  padding: calc(1em + #{$b});
  /* prettifying styles */
	
  &:before, &:after {
    position: absolute;
    z-index: -1; /* put them *behind* parent */
    /* zero all offsets */
    top: 0; right: 0; bottom: 0; left: 0;
    border: solid $b transparent;
    background: $url 50%/ cover 
                  /* background-origin & -clip */
                  border-box;
    content: ''
  }
	
  &:after {
    border-color: rgba(#000, .03);
    background-clip: padding-box;
    backdrop-filter: blur(9px); /* no Firefox support */
  }
}

请记住,此方法的 实时演示 目前不适用于 Firefox,需要在 Chrome 的 chrome://flags 中启用 Experimental Web Platform 特性 才能在 Chrome 中使用。

消除一个伪元素

我不建议在实际环境中这样做,因为它也会切断 Edge 支持,但我们确实有一种方法可以使用一个伪元素来实现我们想要的结果。

我们首先在元素上设置图像背景(只要我们在 padding 中包含其宽度,我们实际上不需要显式设置 border),然后在覆盖其整个父元素的绝对定位伪元素上设置一个部分 transparent、几乎不可见的 background。我们还在这个伪元素上设置了 backdrop-filter

$b: 1.5em; // border-width

div {
  position: relative;
  padding: calc(1em + #{$b});
  background: url(oranges.jpg) 50%/ cover;
  /* prettifying styles */
	
  &:before {
    position: absolute;
    /* zero all offsets */
    top: 0; right: 0; bottom: 0; left: 0;
    background: rgba(#000, .03);
    backdrop-filter: blur(9px); /* no Firefox support */
    content: ''
  }
}

好的,但这会模糊几乎 transparent 伪元素后面的整个元素,包括其文本。这不是错误,这是 backdrop-filter 应该做的事情。

Screenshot.
手头的问题。

为了解决这个问题,我们需要消除(不是使 transparent,在这种情况下完全没有用)伪元素的内部矩形(其边缘距离 border-box 边缘 $b 距离)。

我们有两种方法可以做到这一点。

第一种方法(实时演示)是使用 clip-path 和零宽度隧道 技术。

$b: 1.5em; // border-width
$o: calc(100% - #{$b});

div {
  /* same styles as before */
	
  &:before {
    /* same styles as before */

    /* doesn't work in Edge */
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 
                       0 0, 
                       #{$b $b}, #{$b $o}, #{$o $o}, #{$o $b}, 
                       #{$b $b});
  }
}

第二种方法(实时演示)是使用两个 组合的 mask(请注意,在这种情况下,我们需要在我们的伪元素上显式设置 border)。

$b: 1.5em; // border-width

div {
  /* same styles as before */
	
  &:before {
    /* same styles as before */

    border: solid $b transparent;

    /* doesn't work in Edge */
    --fill: linear-gradient(red, red);
    -webkit-mask: var(--fill) padding-box, 
                  var(--fill);
    -webkit-mask-composite: xor;
            mask: var(--fill) padding-box exclude, 
                  var(--fill);
  }
}

由于这两个属性都不适用于 Edge,这意味着支持现在仅限于 WebKit 浏览器(并且我们仍然需要为 Chrome 中的 backdrop-filter 启用 Experimental Web Platform 特性标志)。

未来的(更好的!)解决方案

filter() 函数允许我们在各个 background 层上应用过滤器。这消除了对伪元素的需求,并将实现此效果所需的代码减少到两个 CSS 声明!

border: solid 1.5em rgba(#000, .03);
background: $url 
              border-box /* background-origin */
              padding-box /* background-clip */, 
            filter($url, blur(9px)) 
              /* background-origin & background-clip */
              border-box

你可能已经猜到了,这里的问题是支持。Safari 是目前唯一实现它的浏览器,但如果你认为 filter() 可以帮助你,你可以添加你的用例并跟踪 ChromeFirefox 的实现进度。

更多边界过滤器选项

到目前为止,我只讨论了对 border 进行模糊处理,但这种技术适用于几乎所有 CSS filter(除了 drop-shadow(),这在当前情况下没有多大意义)。你可以在下面的交互式演示中尝试在它们之间切换并调整值。

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

到目前为止,我们只使用了一个 filter 函数,但我们也可以将它们链接起来,这样可能性就无穷无尽了——你能用这种方式想出什么酷炫的效果?

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