使用 :has() 关系伪类创建动画可点击卡片

Avatar of Brecht De Ruyte
Brecht De Ruyte

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

CSS 的 :has() 伪类正在许多浏览器中推出,**Chrome** 和 **Safari** 已经完全支持它。它通常被称为“父选择器”——就像我们可以从子选择器中选择样式化父元素一样——但 :has() 可以帮助我们解决的问题远不止这些。其中之一就是重新设计许多人喜欢不时使用的可点击卡片模式。

我们将看看 :has() 如何帮助我们处理链接卡片,但首先……

这个 :has() 伪类是什么?

已经有 很多 关于 优秀 的文章 在网络上流传,它们出色地解释了 :has() 是什么以及它的用途,但它仍然足够新,我们也应该在这里说几句话。

:has() 是一个关系伪类,它是 W3C 选择器级别 4 工作草案 的一部分。这就是括号的意义所在:匹配与某些子元素相关(或更准确地说是包含某些子元素)的元素。

/* Matches an article element that contains an image element */
article:has(img) { }

/* Matches an article element with an image contained immediately within it */
article:has(> img) { }

因此,您可以理解为什么我们可能想将其称为“父”选择器。但我们也可以将其与其他功能性伪类结合起来,以获得更具体的匹配。假设我们想为不包含任何图像的文章设置样式。我们可以将 :has() 的关系能力与 :not() 的否定能力结合起来做到这一点。

/* Matches an article without images  */
article:not(:has(img)) { }

但这仅仅是我们如何结合能力来使用 :has() 做更多事情的开始。在我们专门转向解决可点击卡片难题之前,让我们看看在不使用 :has() 的情况下,我们目前如何处理它们。

我们目前如何处理可点击卡片

如今,人们创建完全可点击卡片主要有三种方法,为了充分理解此伪类的强大功能,对其进行总结非常有益。

这种方法经常使用。我从未使用过这种方法,但我创建了一个快速演示来演示它。

这里有很多问题,尤其是在可访问性方面。当用户使用 转子功能 浏览您的网站时,他们会听到该 <a> 元素内的完整文本——标题、文本和链接。有些人可能不想听完所有这些。我们可以做得更好。从 HTML5 开始,我们可以在 <a> 元素内嵌套块级元素。但由于这个原因,它对我来说感觉永远不对。

优点

  • 易于实现
  • 语义正确

缺点

  • 可访问性问题
  • 文本不可选择
  • 覆盖您在默认链接上使用的样式会带来很多麻烦

JavaScript 方法

使用 JavaScript,我们可以将链接附加到卡片上,而不是在标记中编写它。我发现了 costdev 的这个很棒的 CodePen 演示,他还在此过程中使卡片文本可选择。

这种方法有很多好处。我们的链接在获得焦点时是可访问的,我们甚至可以选择文本。但在样式方面有一些缺点。例如,如果我们想为这些卡片制作动画,则必须在我们的主要 .card 包装器上添加 :hover 样式,而不是在链接本身添加。当链接通过键盘制表键获得焦点时,我们也不会从动画中受益。

优点

  • 可以完全实现可访问性
  • 能够选择文本

缺点

  • 需要 JavaScript
  • 无法右键点击(尽管可以通过一些额外的脚本修复)
  • 需要在卡片本身进行大量样式设置,这在聚焦链接时不起作用

::after 选择器方法

此方法要求我们将卡片设置为相对定位,然后在链接的 ::after 伪选择器上设置绝对定位。这不需要任何 JavaScript 并且非常易于实现。

这里有一些缺点,尤其是在选择文本方面。除非您在卡片主体上提供更高的 z-index,否则您将无法选择文本,但如果您这样做,请注意点击文本将不会激活您的链接。您是否希望文本可选择取决于您自己。我认为这可能是一个 UX 问题,但这取决于用例。文本仍然可以通过屏幕阅读器访问,但我对这种方法的主要问题是缺乏动画可能性。

优点

  • 易于实现
  • 可访问的链接,没有冗长的文本
  • 适用于悬停和焦点

缺点

  • 文本不可选择
  • 您只能为链接制作动画,因为这是您悬停的元素。

一种新方法:将 ::after:has() 结合使用

既然我们已经确定了可点击卡片的现有方法,我想展示一下将 :has() 引入组合中是如何解决大多数这些缺点的。

事实上,让我们将这种方法建立在我们之前查看的最后一种方法的基础上,在链接元素上使用 ::after。我们实际上可以在那里使用 :has() 来克服该方法的动画限制。

让我们从标记开始。

<article>
  <figure>
    <img src="cat.webp" alt="Fluffy gray and white tabby kitten snuggled up in a ball." />
  </figure>
  <div clas="article-body">
    <h2>Some Heading</h2>
    <p>Curabitur convallis ac quam vitae laoreet. Nulla mauris ante, euismod sed lacus sit amet, congue bibendum eros. Etiam mattis lobortis porta. Vestibulum ultrices iaculis enim imperdiet egestas.</p>
    <a href="#">
      Read more
       <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 20 20" fill="currentColor">
         <path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd" />
       </svg>
    </a>
  </div>
</article>

我将尽可能地保持简单,在 CSS 中定位元素而不是类。

对于此演示,我们将在悬停时为卡片添加图像缩放和阴影,并为链接制作动画,使其弹出箭头并更改链接的文本颜色。为了简化操作,我们将在卡片上添加一些作用域自定义属性。这是基本样式。

/* The card element */
article {
  --img-scale: 1.001;
  --title-color: black;
  --link-icon-translate: -20px;
  --link-icon-opacity: 0;

  position: relative;
  border-radius: 16px;
  box-shadow: none;
  background: #fff;
  transform-origin: center;
  transition: all 0.4s ease-in-out;
  overflow: hidden;
}
/* The link's ::after pseudo */
article a::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline: 0;
  cursor: pointer;
}

太好了!我们添加了图像的初始缩放比例 (--img-scale: 1.001)、卡片标题的初始颜色 (--title-color: black) 和一些我们将用于使箭头从链接中弹出的额外属性。我们还设置了 box-shadow 声明的空状态以便稍后对其进行动画处理。这为我们现在可点击卡片所需的设置奠定了基础,因此让我们通过将这些自定义属性添加到我们要制作动画的元素中来添加一些重置和样式。

article h2 {
  margin: 0 0 18px 0;
  font-family: "Bebas Neue", cursive;
  font-size: 1.9rem;
  letter-spacing: 0.06em;
  color: var(--title-color);
  transition: color 0.3s ease-out;
}
article figure {
  margin: 0;
  padding: 0;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}
article img {
  max-width: 100%;
  transform-origin: center;
  transform: scale(var(--img-scale));
  transition: transform 0.4s ease-in-out;
}
article a {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: #28666e;
}
article a:focus {
  outline: 1px dotted #28666e;
}
article a .icon {
  min-width: 24px;
  width: 24px;
  height: 24px;
  margin-left: 5px;
  transform: translateX(var(--link-icon-translate));
  opacity: var(--link-icon-opacity);
  transition: all 0.3s;
}

.article-body {
  padding: 24px;
}

让我们对用户友好一些,并在链接后面添加一个 屏幕阅读器类 隐藏。

.sr-only:not(:focus):not(:active) {
  clip: rect(0 0 0 0); 
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap; 
  width: 1px;
}

我们的卡片开始变得非常漂亮。是时候为它添加一些魔法了。使用 :has() 伪类,我们现在可以检查我们的链接是否处于悬停或焦点状态,然后更新我们的自定义属性并添加 box-shadow。有了这段小小的 CSS 代码,我们的卡片真正焕发了生机。

/* Matches an article element that contains a hover or focus state */
article:has(:hover, :focus) {
  --img-scale: 1.1;
  --title-color: #28666e;
  --link-icon-translate: 0;
  --link-icon-opacity: 1;

  box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;
}

看到那里发生了什么了吗?现在,如果卡片中的任何子元素处于悬停或焦点状态,我们都会获得更新的样式。即使在 ::after 可点击卡片方法中,只有链接元素才能包含悬停或焦点状态,我们也可以使用它来匹配父元素并应用过渡。

就是这样。这只是 :has() 选择器的另一个强大的用例。我们不仅可以通过声明其他元素作为参数来匹配父元素,还可以使用伪类来匹配和设置父元素的样式。

优点

  • 可访问的
  • 可动画的
  • 不需要 JavaScript
  • 在正确的元素上使用 :hover

缺点

  • 文本不容易选择。
  • 浏览器支持仅限于 Chrome 和 Safari(在 Firefox 中通过一个标志支持)。

这是一个使用此技术的演示。您可能会注意到卡片周围有一个额外的包装器,但这只是我在使用容器查询,它只是所有主要浏览器中推出的其他一些很棒的功能之一。

您是否有其他示例想分享?欢迎在评论区分享其他解决方案或想法。