构建交互式悬停卡片

当用户对某个链接表示兴趣(通过悬停或聚焦)时,悬停卡片会提供内容的互动式预览。此内容会在用户导航之前显示,既能为用户提供所需内容,又能帮助验证链接是否包含用户正在寻找的内容。

如果您需要纯文本、非交互式提示,而不是结构化链接预览,请参阅悬停时显示提示。

1. 触发悬停卡片

使用 Interest Invokers API 触发悬停卡片。这样可以确保卡片在悬停和聚焦时都会显示,并自动处理无障碍功能连接。

  • 向卡片容器添加 popover="hint" 属性。使用 hint 可避免关闭可能已打开的不相关弹出式菜单。
  • 向触发器链接添加 interestfor 属性,并指向卡片的 id。
<a href="/article" interestfor="article-preview">Article title</a>

<!-- The `id` must match the `interestfor` value on the trigger. -->
<div id="article-preview" popover="hint" class="hovercard">
  <h2>The buzz on hummingbirds</h2>
  <p class="byline">By Delphi Aguilar</p>
  <div>
    <img src="bird.jpg" alt="A vibrant hummingbird">
    <p>Brief summary of the article...</p>
  </div>
</div>

您无需在悬停卡片中添加指向内容的额外链接,因为这与触发链接重复。

interestfor 内置无障碍功能

当您使用 interestfor 时,浏览器会为您处理辅助技术连接:

  • 隐式语义:具有 popover="hint" 的目标会获得 tooltip 的隐式最低角色。请勿自行设置 role="tooltip"。
  • 隐式关联:当目标为纯文本时,浏览器会通过 aria-describedby 将源元素与目标隐式关联;当目标包含互动内容时,浏览器会通过 aria-details 将源元素与目标隐式关联。请勿向触发器添加 aria-describedby 或 aria-details。
  • 互动内容:由于关联会在需要时切换到 aria-details,因此目标允许包含互动内容(例如“兴趣卡片”内的链接)。

2. 使用锚点定位进行定位

使用 CSS 锚点定位,相对于触发器定位悬停卡片。使用 interestfor 时,触发器会成为弹出式窗口的隐式锚点。

[popover].hovercard {
  /* Position the card below the trigger, aligned with its inline-start edge
     and spanning toward the inline-end, flipping if it overflows the viewport. */
  position-area: block-end span-inline-end;
  position-try-fallbacks: flip-block, flip-inline;
}

3. 为进入和退出添加动画效果

默认情况下,对于键盘和鼠标用户,兴趣会在延迟半秒后显示和消失。这样可以防止在通过 Tab 键浏览网页或移动鼠标时打开目标弹出式窗口,以及在将鼠标从触发器移至弹出式窗口时关闭弹出式窗口。如需调整时间,请使用 interest-delay CSS 属性。

[interestfor]{
  /* Shorthand for interest-delay-start and interest-delay-end */
  interest-delay: 0.2s 0.1s;
}

如果用户可能会快速连续触发多个触发器,请在兴趣触发的弹出式窗口已打开的情况下,将初始延迟设置为 0s。

:root:has(:interest-source) [interestfor] {
  interest-delay-start: 0s;
}

使用 @starting-style 定义进入动画,并使用 transition-behavior: allow-discrete(以及 display 和 overlay)为从顶层退出的过程添加动画效果,从而在 @media (prefers-reduced-motion: no-preference) 后面控制过渡。

[popover].hovercard {
  opacity: 0;
  transform-origin: top;
}

[popover].hovercard:popover-open {
  opacity: 1;
  transform: translateY(0) scale(1);
}

@starting-style {
  [popover].hovercard:popover-open {
    opacity: 0;
    transform: translateY(-10px) scale(0.95);
  }
}

/* MANDATORY: Respect user preference for reduced motion by selectively applying transitions. */
@media (prefers-reduced-motion: no-preference) {
  [popover].hovercard {
    transition:
      opacity,
      transform,
      display,
      overlay;
    transition-duration: 0.4s;
    transition-behavior: allow-discrete;
  }
}

4. 可选:无缝过渡到下一页

您可以选择使用跨文档视图过渡,在悬停卡片和目标网页之间创建无缝的“变形”动画。如需了解实现方面的指导,请参阅一致的跨文档过渡。

通过为悬停卡片中的元素及其在下一页中的对应元素分配匹配的 view-transition-name 值,浏览器将在导航过程中为它们添加动画效果。您可以为多个元素(例如标题、图片、署名)命名,以创建复杂的多元素变形效果。使用 :popover-open 将 view-transition-name 分配给打开的弹出式窗口中的元素,以避免创建重复的 view-transition-name 或需要特定于链接的 view-transition-name。

/* Opt-in to cross-document transitions on both pages */
@view-transition {
  navigation: auto;
}

/* Don't show transitions when loading this page */
::view-transition-group(*) {
  animation-duration: 0s;
}

/* Assign names ONLY when the popover is open to ensure only the correct elements are selected. */
[popover]:popover-open {
  view-transition-name: --hovercard;
  view-transition-class: morph;

  h2 {
    view-transition-name: --title;
    view-transition-class: morph;
  }

  img {
    view-transition-name: --image;
    view-transition-class: morph;
  }
}

强制性要求:为确保过渡稳定且不闪烁,目标网页应使用 blocking="render" 和 <link rel="expect"> 等待关键内容解析完毕,然后再渲染第一帧。

<!-- On the destination page: -->
<head>
  <!-- Block rendering until the hero image and title are parsed,
       ensuring the view transition snapshot is taken at the right time. -->
  <link rel="expect" href="#main-content" blocking="render">
</head>
<body>
  <main id="main-content">
    <h1>The buzz on hummingbirds</h1>
    <img src="bird.jpg">
  </main>
</body>
/* On the destination page: */
main {
  view-transition-name: --hovercard;
  view-transition-class: morph;
}
h1 {
  view-transition-name: --title;
  view-transition-class: morph;
}
img {
  view-transition-name: --image;
  view-transition-class: morph;
}

/* By default no transition */
::view-transition-group(*) {
  animation-duration: 0s;
}
/* Short transition for morphing elements */
::view-transition-group(.morph) {
  animation-duration: 0.4s;
}
/* Disable transition if no `old` view */
::view-transition-new(.morph):only-child {
  animation-duration: 0s;
}

后备策略

由于触发器是标准 <a href="..."> 链接,因此如果您选择将悬停卡片视为渐进增强,不支持该功能的浏览器仍可在点击时进行导航。如果需要跨浏览器悬停卡片支持,请根据需要有条件地加载 polyfill。

Popover 的回退和浏览器支持

Popover API 主要是一种渐进增强技术,但其核心行为(顶层提升、轻触关闭和 popovertarget 调用)没有仅限 CSS 的等效项。旧版浏览器需要 polyfill,如果您不想提供 polyfill,则需要手动后备方案。

Polyfill:如需在旧版浏览器中支持 popover 属性,请有条件地加载 @oddbird/popover-polyfill。强制性要求:通过检查 HTMLElement.prototype 上的 popover 属性来检测功能,并且仅在缺少原生支持时加载 polyfill,请勿无条件加载。

使用打包工具或导入映射:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

如果没有打包器,请从 <script type="module"> 内的 CDN 导入:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

样式注意事项:polyfill 无法定义真正的 :popover-open 伪类,因此它会改为应用 .\:popover-open 类。强制性:使用 :is() 或 :where() 将两者结合,否则缺少 :popover-open 的浏览器会舍弃整个规则:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

或者,对于没有 polyfill 的旧版后备方案,请使用 position: fixed 并通过 getBoundingClientRect() 手动计算坐标,或依赖 inset: auto 的默认定位(如果该用例可以接受)。

兴趣调用器的回退和浏览器支持

必须使用 NPM 中的 interestfor polyfill 软件包有条件地对兴趣调用器进行 polyfill。建议捆绑 polyfill,而不是使用 CDN。

强制要求:通过检查 HTMLButtonElement.prototype 上的 interestForElement 属性来检测功能,并且仅在缺少原生支持时加载 polyfill。

<script type="module">
  if (!HTMLButtonElement.prototype.hasOwnProperty("interestForElement")) {
    // CDN link only used for example, prefer bundling.
    await import("https://unpkg.com/interestfor@latest");
  }
</script>

CSS Polyfill 注意事项

使用 polyfill 时,CSS API 会略有变化,以弥补原生伪类和属性支持的不足:

  • 伪类:polyfill 无法定义真正的 :interest-source 和 :interest-target 伪类,因此它会改为应用 .interest-source 和 .interest-target 类。强制性:将它们与 :is() 结合使用,以同时支持原生状态和 polyfill 状态。
/* Styles to apply when the effect is being previewed */
:is(:interest-source, .interest-source) { /* ... */ }
:is(:interest-target, .interest-target) { /* ... */ }
  • CSS 属性:对于 interest-delay、interest-delay-start 和 interest-delay-end,polyfill 需要使用 CSS 变量,因为它无法在所有浏览器中直接读取自定义属性。
/* Adjust the start and end delay for interest invokers */
[interestfor] {
  --interest-delay-start: 0.2s;
  interest-delay-start: var(--interest-delay-start);
  --interest-delay-end: 0.1s;
  interest-delay-end: var(--interest-delay-end);
}