Создайте интерактивную всплывающую карточку

Всплывающие карточки позволяют пользователям увидеть интерактивный предпросмотр контента, когда они наводят указатель на ссылку или выбирают ее. Они показываются пользователям перед переходом по ссылке и либо предоставляют им нужный контент, либо помогают убедиться, что по ссылке будет именно то, что они ищут.

Если вам нужны неинтерактивные подсказки в виде обычного текста, а не структурированный предварительный просмотр ссылок, ознакомьтесь со статьей Как показать подсказку при наведении указателя мыши.

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. Позиционирование с помощью Anchor Positioning

Расположите всплывающую карточку относительно ее триггера с помощью CSS Anchor Positioning. При использовании 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 или перемещении мыши, а также закрытие при перемещении мыши от триггера к всплывающему окну. Чтобы настроить время, используйте свойство CSS interest-delay.

[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 элементам во всплывающей карточке и их аналогам на следующей странице, браузер будет анимировать их при навигации. Вы можете указать несколько элементов (например, заголовки, изображения, поля автора), чтобы создать сложный эффект морфинга. Назначьте view-transition-name элементам во всплывающем окне с помощью :popover-open, чтобы не создавать дублирующиеся 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="...">, браузеры без поддержки по-прежнему могут осуществлять навигацию по клику, если вы решите рассматривать всплывающую карточку как прогрессивное улучшение. Если требуется поддержка всплывающих карточек в разных браузерах, загружайте полифилы по мере необходимости.

Резервные варианты и поддержка Popover в браузерах

Popover API в основном представляет собой прогрессивное улучшение, но его определяющие характеристики – продвижение верхнего слоя, легкое отклонение и вызов popovertarget – не имеют эквивалента только для CSS. Для старых браузеров нужен полифил или резервный вариант, если вы не хотите его использовать.

Полифил. Чтобы обеспечить поддержку атрибута popover в старых браузерах, условно загрузите @oddbird/popover-polyfill. ОБЯЗАТЕЛЬНО. Проверьте наличие свойства popover в HTMLElement.prototype и загрузите полифил, только если встроенная поддержка отсутствует. Не загружайте его без необходимости.

С помощью сборщика или карты импорта:

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

Если вы не используете сборщик, импортируйте из CDN в <script type="module">:

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

Ограничение, связанное со стилями. Полифил не может определить настоящий псевдокласс :popover-open, поэтому вместо него применяется класс .\:popover-open. ОБЯЗАТЕЛЬНО. Объедините эти два правила с помощью :is() или :where(), иначе браузеры, не поддерживающие :popover-open, будут игнорировать правило целиком.

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

В качестве альтернативы для устаревшего резервного варианта без полифила используйте position: fixed и вручную рассчитайте координаты с помощью getBoundingClientRect() или используйте позиционирование по умолчанию с помощью inset: auto, если это допустимо для вашего случая.

Резервные варианты и поддержка Interest Invokers в браузерах

Вызывающие объекты интереса должны быть условно заполнены с помощью пакета полизаполнения interestfor из NPM. Рекомендуем использовать полифил в пакете, а не CDN.

ОБЯЗАТЕЛЬНО. Проверьте, поддерживается ли функция, посмотрев свойство interestForElement в HTMLButtonElement.prototype, и загрузите полифил, только если встроенная поддержка отсутствует.

<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

При использовании полифила CSS API немного меняется, чтобы компенсировать отсутствие поддержки нативных псевдоклассов и свойств:

  • Псевдоклассы. Полифил не может определять настоящие псевдоклассы :interest-source и :interest-target, поэтому вместо них он применяет классы .interest-source и .interest-target. ОБЯЗАТЕЛЬНО: объедините их с :is(), чтобы поддерживать как нативные, так и полифильные состояния.
/* 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 полифил требует использования переменных 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);
}