Cómo conectar elementos entre sí con el posicionamiento del anclaje CSS

¿Cómo conectas un elemento a otro actualmente? Puedes intentar hacer un seguimiento de sus posiciones o usar algún tipo de elemento de wrapper.

<!-- index.html -->
<div class="container">
  <a href="/link" class="anchor">I’m the anchor</a>
  <div class="anchored">I’m the anchored thing</div>
</div>
/* styles.css */
.container {
  position: relative;
}
.anchored {
  position: absolute;
}

Estas soluciones suelen no ser ideales. Necesitan JavaScript o introducen un marcado adicional. El objetivo de la API de posicionamiento de anclas de CSS es resolver este problema proporcionando una API de CSS para elementos de conexión. Proporciona un medio para posicionar y ajustar el tamaño de un elemento según la posición y el tamaño de otros elementos.

La imagen muestra una ventana de navegador de maqueta que detalla la anatomía de una herramienta de ayuda.

Navegadores compatibles

Puedes probar la API de posicionamiento de anclas CSS en Chrome Canary detrás de la marca de función "Funciones experimentales de la plataforma web". Para habilitar esa marca, abre Chrome Canary y visita chrome://flags. Luego, habilita la marca "Funciones experimentales de la plataforma web".

El equipo de Oddbird también está desarrollando un polyfill. Asegúrate de revisar el repositorio en github.com/oddbird/css-anchor-positioning.

Puedes verificar la compatibilidad con la fijación con lo siguiente:

@supports(anchor-name: --foo) {
  /* Styles... */
}

Ten en cuenta que esta API aún se encuentra en una etapa experimental y podría cambiar. En este artículo, se describen las partes importantes de forma general. La implementación actual tampoco está completamente sincronizada con la especificación del grupo de trabajo de CSS.

El problema

¿Por qué deberías hacer esto? Un caso de uso destacado sería la creación de cuadros de herramientas o experiencias similares. En ese caso, a menudo es conveniente vincular la información sobre herramientas al contenido al que hace referencia. A menudo, es necesario vincular un elemento a otro. También esperas que interactuar con la página no rompa esa vinculación, por ejemplo, si un usuario desplaza o cambia el tamaño de la IU.

Otra parte del problema es si quieres asegurarte de que el elemento conectado permanezca en la vista, por ejemplo, si abres una información sobre herramientas y se recorta por los límites del viewport. Es posible que esta no sea una experiencia del usuario muy buena. Quieres que la información sobre herramientas se adapte.

Soluciones actuales

Actualmente, existen diferentes formas de abordar el problema.

En primer lugar, está el enfoque rudimentario "Unir el ancla". Tomas ambos elementos y los unes en un contenedor. Luego, puedes usar position para posicionar la información sobre herramientas en relación con el ancla.

<div class="containing-block">
  <div class="tooltip">Anchor me!</div>
  <a class="anchor">The anchor</a>
</div>
.containing-block {
  position: relative;
}

.tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
}

Puedes mover el contenedor y todo permanecerá donde quieras, en su mayor parte.

Otro enfoque podría ser si conoces la posición de tu ancla o si puedes hacer un seguimiento de ella de alguna manera. Puedes pasarlo a la información sobre herramientas con propiedades personalizadas.

<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
  --anchor-width: 120px;
  --anchor-top: 40vh;
  --anchor-left: 20vmin;
}

.anchor {
  position: absolute;
  top: var(--anchor-top);
  left: var(--anchor-left);
  width: var(--anchor-width);
}

.tooltip {
  position: absolute;
  top: calc(var(--anchor-top));
  left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
  transform: translate(-50%, calc(-100% - 10px));
}

Pero, ¿qué sucede si no conoces la posición de tu ancla? Es probable que debas intervenir con JavaScript. Podrías hacer algo como lo que hace el siguiente código, pero esto significa que tus estilos comienzan a salirse del CSS y a pasar a JavaScript.

const setAnchorPosition = (anchored, anchor) => {
  const bounds = anchor.getBoundingClientRect().toJSON();
  for (const [key, value] of Object.entries(bounds)) {
    anchored.style.setProperty(`--${key}`, value);
  }
};

const update = () => {
  setAnchorPosition(
    document.querySelector('.tooltip'),
    document.querySelector('.anchor')
  );
};

window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);

Esto comienza a plantear algunas preguntas:

  • ¿Cuándo debo calcular los estilos?
  • ¿Cómo calculo los estilos?
  • ¿Con qué frecuencia debo calcular los estilos?

¿Se resolvió el problema? Puede ser para tu caso de uso, pero hay un problema: nuestra solución no se adapta. No es responsivo. ¿Qué sucede si el viewport corta mi elemento anclado?

Ahora debes decidir si quieres reaccionar a esto y cómo hacerlo. La cantidad de preguntas y decisiones que debes tomar empieza a crecer. Todo lo que quieres hacer es fijar un elemento a otro. En un mundo ideal, tu solución se ajustará y reaccionará a su entorno.

Para aliviar un poco ese dolor, puedes buscar una solución de JavaScript que te ayude. Eso generará el costo de agregar una dependencia a tu proyecto y podría generar problemas de rendimiento según cómo los uses. Por ejemplo, algunos paquetes usan requestAnimationFrame para mantener la posición correcta. Esto significa que tú y tu equipo deben familiarizarse con el paquete y sus opciones de configuración. Como resultado, es posible que tus preguntas y decisiones no se reduzcan, sino que cambien. Esto forma parte del “por qué” del posicionamiento de anclaje de CSS. Te permitirá no tener que pensar en problemas de rendimiento cuando calcules la posición.

Este es el aspecto que podría tener el código para usar "floating-ui", un paquete popular para este problema:

import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';

const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')

const updatePosition = () => {  
  computePosition(anchor, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip()]
  })
    .then(({x, y}) => {
      Object.assign(tooltip.style, {
        left: `${x}px`,
        top: `${y}px`
      })
  })
};

const clean = autoUpdate(anchor, tooltip, updatePosition);

Intenta volver a posicionar el ancla en esta demostración que usa ese código.

Es posible que la "ventana de información" no se comporte como esperas. Reacciona cuando se sale del viewport en el eje Y, pero no en el eje X. Analiza la documentación y es probable que encuentres una solución que funcione para ti.

Sin embargo, encontrar un paquete que funcione para tu proyecto puede llevar mucho tiempo. Son decisiones adicionales y pueden ser frustrantes si no funcionan como deseas.

Cómo usar el posicionamiento de ancla

Ingresa la API de posicionamiento de anclaje de CSS. La idea es mantener tus estilos en el CSS y reducir la cantidad de decisiones que debes tomar. Esperas obtener el mismo resultado, pero el objetivo es mejorar la experiencia del desarrollador.

  • No es necesario usar JavaScript.
  • Permite que el navegador determine la mejor posición a partir de tu guía.
  • No más dependencias de terceros
  • No hay elementos de wrapper.
  • Funciona con elementos que se encuentran en la capa superior.

Vamos a recrear y abordar el problema que intentábamos resolver antes. En su lugar, usa la analogía de un barco con una ancla. Estos representan el elemento y el ancla. El agua representa el bloque contenedor.

Primero, debes elegir cómo definir el ancla. Para ello, configura la propiedad anchor-name en el elemento de anclaje en tu CSS. Acepta un valor de ident con guiones.

.anchor {
  anchor-name: --my-anchor;
}

Como alternativa, podrás definir un ancla en tu código HTML con el atributo anchor. El valor del atributo es el ID del elemento de anclaje. Esto crea un ancla implícita.

<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a boat!</div>

Una vez que hayas definido un ancla, puedes usar la función anchor. La función anchor toma 3 argumentos:

  • Elemento de anclaje: Es el anchor-name del ancla que se usará. También puedes omitir el valor para usar un ancla implicit. Se puede definir a través de la relación HTML o con una propiedad anchor-default con un valor anchor-name.
  • Lado de anclaje: Es una palabra clave de la posición que deseas usar. Puede ser top, right, bottom, left, center, etcétera. También puedes pasar un porcentaje. Por ejemplo, el 50% sería igual a center.
  • Uso alternativo: Es un valor de resguardo opcional que acepta una longitud o un porcentaje.

Usas la función anchor como valor para las propiedades de desplazamiento (top, right, bottom, left o sus equivalentes lógicos) del elemento anclado. También puedes usar la función anchor en calc:

.boat {
  bottom: anchor(--my-anchor top);
  left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}

 /* alternative with anchor-default */
.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: calc(anchor(center) - (var(--boat-size) * 0.5));
}

No hay una propiedad de desplazamiento center, por lo que una opción es usar calc si conoces el tamaño de tu elemento anclado. ¿Por qué no usar translate? Puedes usar lo siguiente:

.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
}

Sin embargo, el navegador no tiene en cuenta las posiciones transformadas para los elementos anclados. Quedará claro por qué esto es importante cuando consideres los resguardos de posición y el posicionamiento automático.

Es posible que hayas notado el uso de la propiedad personalizada --boat-size anterior. Sin embargo, si deseas basar el tamaño del elemento anclado en el del ancla, también puedes acceder a ese tamaño. En lugar de calcularlo por tu cuenta, puedes usar la función anchor-size. Por ejemplo, para hacer que nuestro barco sea cuatro veces el ancho de nuestra ancla, haz lo siguiente:

.boat {
  width: calc(4 * anchor-size(--my-anchor width));
}

También tienes acceso a la altura con anchor-size(--my-anchor height). Además, puedes usarlo para establecer el tamaño de uno o ambos ejes.

¿Qué sucede si quieres fijar un elemento con posicionamiento absolute? La regla es que los elementos no pueden ser hermanos. En ese caso, puedes unir el ancla con un contenedor que tenga posicionamiento relative. Luego, puedes fijarlo.

<div class="anchor-wrapper">
  <a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a boat!</div>

Mira esta demostración en la que puedes arrastrar el ancla y el barco la seguirá.

Cómo hacer un seguimiento de la posición de desplazamiento

En algunos casos, es posible que el elemento de anclaje esté dentro de un contenedor desplazable. Al mismo tiempo, es posible que el elemento anclado esté fuera de ese contenedor. Como el desplazamiento se produce en un subproceso diferente del diseño, necesitas una forma de hacerle un seguimiento. La propiedad anchor-scroll puede hacer esto. Se configura en el elemento anclado y se le asigna el valor del ancla al que deseas hacer un seguimiento.

.boat { anchor-scroll: --my-anchor; }

Prueba esta demostración en la que puedes activar y desactivar anchor-scroll con la casilla de verificación en la esquina.

Sin embargo, la analogía no es del todo precisa, ya que, en un mundo ideal, el barco y el ancla están en el agua. Además, las funciones como la API de Popover permiten mantener los elementos relacionados cerca. Sin embargo, el posicionamiento de anclas funcionará con los elementos que se encuentran en la capa superior. Este es uno de los principales beneficios de la API: poder conectar elementos en diferentes flujos.

Considera esta demostración que tiene un contenedor de desplazamiento con anclas que tienen información sobre herramientas. Es posible que los elementos de la información sobre herramientas que son ventanas emergentes no se encuentren en la misma ubicación que los anclajes:

Sin embargo, notarás cómo los popovers hacen un seguimiento de sus respectivos vínculos de anclaje. Puedes cambiar el tamaño de ese contenedor de desplazamiento y las posiciones se actualizarán por ti.

Posicionamiento de resguardo y posicionamiento automático

Aquí es donde el poder de posicionamiento de los anclajes sube de nivel. Un position-fallback puede posicionar tu elemento anclado según un conjunto de resguardos que proporciones. Guías al navegador con tus estilos y le permites que determine la posición por ti.

El caso de uso común aquí es una información sobre herramientas que debe alternar entre mostrarse arriba o debajo de un ancla. Y este comportamiento se basa en si el contenedor recortaría la información sobre herramientas. Por lo general, ese contenedor es la ventana de visualización.

Si analizaste el código de la última demostración, habrías visto que había una propiedad position-fallback en uso. Si desplazaste el contenedor, es posible que hayas notado que esos pop-overs anclados saltaron. Esto sucedió cuando sus respectivos anclajes se acercaron al límite del viewport. En ese momento, los popovers intentan ajustarse para permanecer en la vista del puerto.

Antes de crear un position-fallback explícito, el posicionamiento de ancla también ofrecerá posicionamiento automático. Puedes obtener ese giro de forma gratuita usando un valor de auto en la función de anclaje y en la propiedad de desplazamiento opuesto. Por ejemplo, si usas anchor para bottom, establece top como auto.

.tooltip {
  position: absolute;
  bottom: anchor(--my-anchor auto);
  top: auto;
}

La alternativa al posicionamiento automático es usar un position-fallback explícito. Para ello, debes definir un conjunto de resguardo de posición. El navegador los revisará hasta encontrar uno que pueda usar y, luego, aplicará ese posicionamiento. Si no encuentra uno que funcione, se usará de forma predeterminada el primero que se definió.

Un position-fallback que intenta mostrar las indicaciones sobre herramientas arriba y abajo podría verse de la siguiente manera:

@position-fallback --top-to-bottom {
  @try {
    bottom: anchor(top);
    left: anchor(center);
  }

  @try {
    top: anchor(bottom);
    left: anchor(center);
  }
}

Si aplicas esto a las ventanas de información, se verá de la siguiente manera:

.tooltip {
  anchor-default: --my-anchor;
  position-fallback: --top-to-bottom;
}

El uso de anchor-default significa que puedes volver a usar position-fallback para otros elementos. También puedes usar una propiedad personalizada centrada para establecer anchor-default.

Considera esta demostración con el barco de nuevo. Hay un position-fallback establecido. A medida que cambies la posición del ancla, el barco se ajustará para permanecer dentro del contenedor. Intenta cambiar también el valor de padding, que ajusta el padding del cuerpo. Observa cómo el navegador corrige el posicionamiento. Las posiciones se cambian cambiando la alineación de la cuadrícula del contenedor.

Esta vez, position-fallback es más detallado y prueba posiciones en el sentido de las manecillas del reloj.

.boat {
  anchor-default: --my-anchor;
  position-fallback: --compass;
}

@position-fallback --compass {
  @try {
    bottom: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    right: anchor(left);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }
}


Ejemplos

Ahora que tienes una idea de las funciones principales para el posicionamiento de anclas, veamos algunos ejemplos interesantes más allá de las herramientas de ayuda. El objetivo de estos ejemplos es que fluyan tus ideas sobre cómo podrías usar el posicionamiento de ancla. La mejor manera de llevar las especificaciones más allá es con la entrada de usuarios reales como tú.

Menús contextuales

Comencemos con un menú contextual con la API de Popover. La idea es que, si haces clic en el botón con el signo de v, se mostrará un menú contextual. Y ese menú tendrá su propio menú para expandirse.

El marcado no es la parte importante aquí. Sin embargo, tienes tres botones que usan popovertarget. Luego, tienes tres elementos que usan el atributo popover. Esto te permite abrir los menús contextuales sin ningún JavaScript. Podría verse de la siguiente manera:

<button popovertarget="context">
  Toggle Menu
</button>        
<div popover="auto" id="context">
  <ul>
    <li><button>Save to your Liked Songs</button></li>
    <li>
      <button popovertarget="playlist">
        Add to Playlist
      </button>
    </li>
    <li>
      <button popovertarget="share">
        Share
      </button>
    </li>
  </ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>

Ahora, puedes definir un position-fallback y compartirlo entre los menús contextuales. También nos aseguramos de no establecer ningún estilo inset para los pop-ups.

[popovertarget="share"] {
  anchor-name: --share;
}

[popovertarget="playlist"] {
  anchor-name: --playlist;
}

[popovertarget="context"] {
  anchor-name: --context;
}

#share {
  anchor-default: --share;
  position-fallback: --aligned;
}

#playlist {
  anchor-default: --playlist;
  position-fallback: --aligned;
}

#context {
  anchor-default: --context;
  position-fallback: --flip;
}

@position-fallback --aligned {
  @try {
    top: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }

  @try {
    top: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(bottom);
    left: anchor(right);
  }

  @try {
    right: anchor(left);
    bottom: anchor(bottom);
  }
}

@position-fallback --flip {
  @try {
    bottom: anchor(top);
    left: anchor(left);
  }

  @try {
    right: anchor(right);
    bottom: anchor(top);
  }

  @try {
    top: anchor(bottom);
    left: anchor(left);
  }

  @try {
    top: anchor(bottom);
    right: anchor(right);
  }
}

Esto te brinda una IU de menú contextual anidado y adaptable. Intenta cambiar la posición del contenido con la selección. La opción que elijas actualizará la alineación de la cuadrícula. Esto afecta la forma en que el posicionamiento de la ancla posiciona los pop-ups.

Enfoque y seguimiento

En esta demostración, se combinan primitivas de CSS con :has(). La idea es hacer la transición de un indicador visual para el input que tiene el foco.

Para ello, establece un nuevo ancla en el tiempo de ejecución. En esta demostración, se actualiza una propiedad personalizada centrada en el enfoque de entrada.

#email {
    anchor-name: --email;
  }
  #name {
    anchor-name: --name;
  }
  #password {
    anchor-name: --password;
  }
:root:has(#email:focus) {
    --active-anchor: --email;
  }
  :root:has(#name:focus) {
    --active-anchor: --name;
  }
  :root:has(#password:focus) {
    --active-anchor: --password;
  }

:root {
    --active-anchor: --name;
    --active-left: anchor(var(--active-anchor) right);
    --active-top: calc(
      anchor(var(--active-anchor) top) +
        (
          (
              anchor(var(--active-anchor) bottom) -
                anchor(var(--active-anchor) top)
            ) * 0.5
        )
    );
  }
.form-indicator {
    left: var(--active-left);
    top: var(--active-top);
    transition: all 0.2s;
}

Pero ¿cómo podrías llevar esto más allá? Puedes usarlo para alguna forma de superposición instructiva. Una información sobre herramientas podría moverse entre puntos de interés y actualizar su contenido. Puedes hacer una transición entre el contenido. Aquí podrían funcionar las animaciones discretas que te permiten animar display o las transiciones de vistas.

Cálculo del gráfico de barras

Otra cosa divertida que puedes hacer con el posicionamiento de anclas es combinarlo con calc. Imagina un gráfico en el que tienes algunos pop-overs que anotan el gráfico.

Puedes hacer un seguimiento de los valores más altos y más bajos con min y max de CSS. El CSS para eso podría verse de la siguiente manera:

.chart__tooltip--max {
    left: anchor(--chart right);
    bottom: max(
      anchor(--anchor-1 top),
      anchor(--anchor-2 top),
      anchor(--anchor-3 top)
    );
    translate: 0 50%;
  }

Hay un poco de JavaScript en juego para actualizar los valores del gráfico y un poco de CSS para aplicarle diseño. Sin embargo, el posicionamiento de ancla se encarga de las actualizaciones del diseño por nosotros.

Controles de cambio de tamaño

No es necesario que ancles solo a un elemento. Puedes usar muchos anclajes para un elemento. Es posible que lo hayas notado en el ejemplo del gráfico de barras. Las indicaciones sobre herramientas se anclaron al gráfico y, luego, a la barra correspondiente. Si llevaras ese concepto un poco más lejos, podrías usarlo para cambiar el tamaño de los elementos.

Puedes tratar los puntos de anclaje como controladores de cambio de tamaño personalizados y usar un valor inset.

.container {
   position: absolute;
   inset:
     anchor(--handle-1 top)
     anchor(--handle-2 right)
     anchor(--handle-2 bottom)
     anchor(--handle-1 left);
 }

En esta demostración, GreenSock Draggable hace que los controladores sean Draggable. Sin embargo, el elemento <img> cambia de tamaño para llenar el contenedor que se ajusta para llenar el espacio entre los controladores.

¿Un SelectMenu?

Esta última es una pequeña muestra de lo que está por venir. Sin embargo, puedes crear un popover que se pueda enfocar y, ahora, tienes el posicionamiento de ancla. Podrías crear los fundamentos de un elemento <select> con diseño.

<div class="select-menu">
<button popovertarget="listbox">
 Select option
 <svg>...</svg>
</button>
<div popover="auto" id="listbox">
   <option>A</option>
   <option>Styled</option>
   <option>Select</option>
</div>
</div>

Un anchor implícito facilitará esto. Sin embargo, el CSS para un punto de partida rudimentario podría verse de la siguiente manera:

[popovertarget] {
 anchor-name: --select-button;
}
[popover] {
  anchor-default: --select-button;
  top: anchor(bottom);
  width: anchor-size(width);
  left: anchor(left);
}

Combina las funciones de la API de Popover con el posicionamiento de anclaje de CSS y estarás cerca.

Es muy útil cuando empiezas a introducir elementos como :has(). Puedes rotar el marcador cuando está abierto:

.select-menu:has(:open) svg {
  rotate: 180deg;
}

¿A dónde podrías llevarlo a continuación? ¿Qué más necesitamos para que funcione select? Lo dejaremos para el próximo artículo. Pero no te preocupes, pronto lanzaremos los elementos select que se pueden aplicar estilos. ¡No te pierdas ninguna novedad!


Eso es todo.

La plataforma web evoluciona. El posicionamiento de los anclajes de CSS es una parte fundamental para mejorar la forma en que desarrollas los controles de la IU. Te abstraerá de algunas de esas decisiones difíciles. Pero también te permitirá hacer cosas que nunca antes pudiste hacer. Como aplicar diseño a un elemento <select>. Danos tu opinión.

Foto de CHUTTERSNAP en Unsplash