Cómo compilar una tarjeta flotante interactiva

Las tarjetas flotantes proporcionan una vista previa interactiva del contenido cuando un usuario expresa interés en un vínculo (colocando el cursor sobre él o enfocándolo). Se muestra a los usuarios antes de que naveguen y les proporciona el contenido que necesitan o ayuda a verificar que el vínculo contendrá el contenido que buscan.

Si necesitas información sobre herramientas de texto sin formato no interactiva en lugar de vistas previas de vínculos estructuradas, consulta Muestra información sobre herramientas cuando se coloca el cursor sobre un elemento.

1. Cómo activar la tarjeta emergente

Usa la API de Interest Invokers para activar la tarjeta emergente. Esto garantiza que la tarjeta aparezca tanto al colocar el cursor sobre ella como al enfocarla, y controla automáticamente la conexión de accesibilidad.

  • Agrega el atributo popover="hint" al contenedor de tu tarjeta. Usa hint para evitar cerrar menús emergentes no relacionados que puedan estar abiertos.
  • Agrega el atributo interestfor al vínculo activador, que apunta al id de la tarjeta.
<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>

No es necesario que incluyas un vínculo adicional al contenido dentro de la tarjeta emergente, ya que es redundante con el vínculo de activación.

Accesibilidad integrada en interestfor

Cuando usas interestfor, el navegador controla el cableado de la tecnología de accesibilidad por ti:

  • Semántica implícita: Un destino con popover="hint" obtiene un rol mínimo implícito de tooltip. NO configures role="tooltip" por tu cuenta.
  • Asociación implícita: El navegador asocia de forma implícita el elemento de origen con el destino a través de aria-describedby cuando el destino es texto sin formato o a través de aria-details cuando el destino contiene contenido interactivo. NO agregues aria-describedby ni aria-details al activador.
  • Contenido interactivo: Debido a que la asociación cambia a aria-details cuando es necesario, el destino SÍ puede contener contenido interactivo (p.ej., un vínculo dentro de una "tarjeta de interés").

2. Posicionamiento con Anchor Positioning

Posiciona la tarjeta flotante en relación con su activador usando el posicionamiento de anclaje de CSS. Cuando se usa interestfor, el activador se convierte en un ancla implícita para el cuadro emergente.

[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. Cómo animar la entrada y la salida

De forma predeterminada, el interés se muestra y se pierde para los usuarios de teclado y mouse con un retraso de medio segundo. Esto evita que se abra el cuadro emergente de destino mientras navegas por una página o mueves el mouse, y que se cierre mientras mueves el mouse del activador al cuadro emergente. Para ajustar el tiempo, usa la propiedad de CSS interest-delay.

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

Si es probable que los usuarios pasen por varios activadores en una sucesión rápida, establece el retraso inicial en 0s si ya hay un popover activado por interés abierto.

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

Usa @starting-style para definir la animación de entrada y transition-behavior: allow-discrete (junto con display y overlay) para animar la salida de la capa superior, lo que restringe las transiciones detrás de @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. OPCIONAL: Transición fluida a la siguiente página

De manera opcional, puedes usar las transiciones de vistas entre documentos para crear una animación de "transformación" fluida entre la tarjeta emergente y la página de destino. Consulta Transiciones coherentes entre documentos para obtener orientación sobre la implementación.

Si asignas valores de view-transition-name coincidentes a los elementos de la tarjeta emergente y a sus contrapartes en la página siguiente, el navegador los animará durante la navegación. Puedes nombrar varios elementos (p.ej., títulos, imágenes, firmas) para crear un efecto de transformación complejo con varios elementos. Asigna el view-transition-name a los elementos del cuadro emergente abierto con :popover-open para evitar crear view-transition-name duplicados o requerir view-transition-name específicos del vínculo.

/* 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;
  }
}

OBLIGATORIO: Para garantizar que la transición sea estable y no parpadee, la página de destino debe usar blocking="render" y <link rel="expect"> para esperar a que se analice el contenido crítico antes de renderizar el primer fotograma.

<!-- 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;
}

Estrategias de resguardo

Debido a que el activador es un vínculo <a href="..."> estándar, los navegadores sin compatibilidad aún pueden navegar cuando se hace clic si eliges tratar la tarjeta emergente como una mejora progresiva. Cuando se requiera compatibilidad con tarjetas emergentes en varios navegadores, carga polyfills de forma condicional según sea necesario.

Resguardos y compatibilidad de navegadores para Popover

La API de Popover es, en su mayor parte, una mejora progresiva, pero sus comportamientos definitorios (promoción de la capa superior, cierre rápido y la invocación de popovertarget) no tienen un equivalente solo de CSS. Los navegadores más antiguos necesitan un polyfill o un resguardo manual si prefieres no enviar uno.

Polyfill: Para admitir el atributo popover en navegadores más antiguos, carga @oddbird/popover-polyfill de forma condicional. OBLIGATORIO: Detecta la función verificando la propiedad popover en HTMLElement.prototype y carga el polyfill solo cuando no haya compatibilidad nativa. NO lo cargues de forma incondicional.

Con un empaquetador o un mapa de importación:

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

Sin un empaquetador, importa desde una CDN dentro de un <script type="module">:

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

Advertencia de diseño: El polyfill no puede definir la pseudoclase :popover-open real, por lo que aplica una clase .\:popover-open en su lugar. OBLIGATORIO: Combina las dos con :is() o :where(); de lo contrario, los navegadores que no tengan :popover-open descartarán toda la regla:

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

Como alternativa, para un resguardo heredado sin un polyfill, usa position: fixed y calcula manualmente las coordenadas a través de getBoundingClientRect() o confía en el posicionamiento predeterminado con inset: auto si eso es aceptable para el caso de uso.

Resguardos y compatibilidad con navegadores para los invocadores de interés

Los activadores de interés deben tener un polyfill condicional con el paquete de polyfill interestfor de NPM. Se prefiere agrupar el polyfill en lugar de usar la CDN.

OBLIGATORIO: Detecta la función verificando la propiedad interestForElement en HTMLButtonElement.prototype y carga el polyfill solo cuando no haya compatibilidad nativa.

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

Advertencias sobre el polyfill de CSS

Cuando se usa el polyfill, la API de CSS cambia un poco para compensar la falta de compatibilidad con las pseudoclases y propiedades nativas:

  • Pseudoclases: El polyfill no puede definir pseudoclases :interest-source y :interest-target reales, por lo que aplica las clases .interest-source y .interest-target en su lugar. OBLIGATORIO: Combínalas con :is() para admitir los estados nativos y con polyfill.
/* Styles to apply when the effect is being previewed */
:is(:interest-source, .interest-source) { /* ... */ }
:is(:interest-target, .interest-target) { /* ... */ }
  • Propiedades CSS: Para interest-delay, interest-delay-start y interest-delay-end, el polyfill requiere el uso de variables CSS, ya que no puede leer las propiedades personalizadas directamente en todos los navegadores.
/* 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);
}