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

¿Cómo se conecta actualmente un elemento a otro? Puedes intentar hacer un seguimiento de sus posiciones o usar algún tipo de elemento 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;
}

A menudo, estas soluciones no son las ideales. Necesitan JavaScript o tienen lenguaje de marcado adicional. El objetivo de la API de posicionamiento de anclajes de CSS es resolver esto proporcionando una API de CSS para la conexión de elementos. Proporciona un medio para posicionar y ajustar el tamaño de un elemento en función de la posición y el tamaño de los demás elementos.

La imagen muestra una ventana de simulación del navegador en la que se detalla la anatomía de un cuadro de información.

Navegadores compatibles

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

También hay un polyfill en desarrollo del equipo de Oddbird. Asegúrate de consultar el repo en github.com/oddbird/css-anchor-positioning.

Puedes verificar la compatibilidad con anclas con lo siguiente:

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

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

El problema

¿Por qué necesitas hacer esto? Un caso de uso destacado sería crear cuadros de información o experiencias similares a estos. En ese caso, a menudo querrás conectar la información sobre la herramienta al contenido al que hace referencia. A menudo, se necesita alguna forma de anclar un elemento a otro. También esperas que la interacción con la página no interrumpa ese anclaje a red (por ejemplo, si un usuario se desplaza o cambia el tamaño de la IU).

Otra parte del problema es si quieres asegurarte de que el elemento conectado permanezca a la vista, por ejemplo, si abres un cuadro de información y queda recortado por los límites del viewport. Esta podría no ser una gran experiencia para los usuarios. Quieres que la información sobre la herramienta se adapte.

Soluciones actuales

Actualmente, hay algunas formas diferentes de abordar el problema.

En primer lugar, veremos el rudimentario enfoque “encapsular el ancla”. Tomas ambos elementos y los unes en un contenedor. Luego, puedes usar position para posicionar la información sobre la herramienta 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á en la mayor parte del lugar donde deseas.

Otro enfoque podría ser si conoces la posición de tu ancla o si puedes hacer un seguimiento de ella de alguna manera. Podrías pasarlo al cuadro de información 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é pasa si no sabes la posición de tu ancla? Es probable que debas intervenir con JavaScript. Podrías hacer algo similar a lo que hace el siguiente código, pero ahora esto significa que tus estilos están empezando a filtrarse de CSS 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 calculo los estilos?
  • ¿Cómo calculo los estilos?
  • ¿Con qué frecuencia puedo calcular los estilos?

¿Eso lo resuelve? Podría ser para tu caso de uso, pero hay un problema: nuestra solución no se adapta. No responde. ¿Qué sucede si el viewport corta mi elemento fijo?

Ahora debes decidir si reaccionar a esto y cómo. La cantidad de preguntas y decisiones que necesitas tomar está empezando a aumentar. Todo lo que deseas hacer es anclar un elemento a otro. En un mundo ideal, tu solución se ajustará y reaccionará a su entorno.

Para aliviar algo de 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 la forma en que las 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. Este es uno de los motivos por los que se aplica el posicionamiento de las anclas de CSS. De esta forma, no tendrás que pensar en los problemas de rendimiento al calcular la posición.

A continuación, te mostramos cómo se vería 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 reubicar el ancla en esta demostración que usa ese código.

.

Es posible que la información sobre la herramienta no se comporte como esperas. Reacciona a salir del viewport en el eje Y, pero no en el eje X. Revisa 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 quieres.

Cómo usar el posicionamiento de anclas

Ingresa la API de posicionamiento de anclas de CSS. La idea es mantener los estilos en el CSS y reducir la cantidad de decisiones que debes tomar. Esperas obtener el mismo resultado, pero tu objetivo es mejorar la experiencia de los desarrolladores.

  • No se requiere JavaScript.
  • Deja que el navegador seleccione la mejor posición a partir de tus indicaciones.
  • No hay más dependencias de terceros
  • Sin elementos wrapper
  • Funciona con elementos que se encuentran en la capa superior.

Recreemos y abordemos el problema que estábamos tratando de resolver anteriormente. Pero, en cambio, usa la analogía de un barco con un ancla. Estos representan el elemento anclado y el ancla. El agua representa el bloque contenedor.

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

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

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

<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 mediante la relación HTML o con una propiedad anchor-default con un valor anchor-name.
  • Lado del anclaje: Es una palabra clave de la posición que deseas usar. Podría ser top, right, bottom, left, center, etc. También puedes pasar un porcentaje. Por ejemplo, un 50% sería igual a center.
  • Resguardo: Es un valor de resguardo opcional que acepta una longitud o un porcentaje.

Usa la función anchor como un valor para las propiedades de inserción (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 inserción center, por lo que una opción es usar calc si conoces el tamaño del elemento fijo. ¿Por qué no usar translate? Podrías usar esto:

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

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

Es posible que hayas notado el uso de la propiedad personalizada --boat-size que aparece más arriba. 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 el barco sea cuatro veces el ancho del ancla:

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

También tienes acceso a la altura con anchor-size(--my-anchor height). Y lo puedes usar para configurar el tamaño de cualquiera de los ejes o ambos.

¿Qué sucede si deseas anclar a un elemento con el posicionamiento absolute? La regla es que los elementos no pueden ser elementos del mismo nivel. En ese caso, puedes unir el ancla con un contenedor que tenga posicionamiento relative. Luego, puedes anclarlo.

<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 para que el barco te siga.

Seguimiento de la posición de desplazamiento

En algunos casos, el elemento de anclaje podría estar dentro de un contenedor de desplazamiento. Al mismo tiempo, el elemento fijo puede estar fuera de ese contenedor. Como el desplazamiento ocurre en un subproceso diferente del diseño, necesitas una forma de rastrearlo. La propiedad anchor-scroll puede hacerlo. Lo configuras en el elemento fijo y le asignas el valor del ancla al que deseas hacer seguimiento.

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

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

Sin embargo, la analogía es un poco plana aquí, ya que en un mundo ideal, el barco y el ancla están en el agua. Además, funciones como la API de Popover promueven la posibilidad de mantener cerca los elementos relacionados. Sin embargo, el posicionamiento fijo funcionará con elementos que se encuentren en la capa superior. Este es uno de los principales beneficios de la API: la capacidad de conectar elementos en diferentes flujos.

Considera esta demostración que tiene un contenedor de desplazamiento con anclas que tienen información sobre la herramienta. Es posible que los elementos de información de la herramienta que son ventanas emergentes no se ubiquen junto con las anclas:

Sin embargo, observarás cómo las ventanas emergentes rastrean sus respectivos vínculos de anclaje. Puedes cambiar el tamaño del contenedor de desplazamiento y las posiciones se actualizarán automáticamente.

Resguardo de posición y posicionamiento automático

Aquí es donde el poder de posicionamiento de un ancla sube de nivel. Un position-fallback puede posicionar tu elemento fijo en función de un conjunto de resguardos que proporciones. Guías el navegador con tus estilos y dejas que se adapte a tu posición.

Un caso de uso común aquí es la información sobre la herramienta que debería variar entre mostrarse arriba o debajo de un ancla. Este comportamiento se basa en si la información sobre la herramienta se recortaría por su contenedor. Ese contenedor suele ser el viewport.

Si hubieras explorado el código de la última demostración, habrías notado que había una propiedad position-fallback en uso. Si te desplazaste por el contenedor, es posible que hayas notado que esas ventanas emergentes fijas saltaron. Esto sucedió cuando sus respectivas anclas se acercaron al límite del viewport. En ese momento, las ventanas emergentes están intentando ajustarse para permanecer en el viewport.

Antes de crear un position-fallback explícito, el posicionamiento de anclaje también ofrecerá el posicionamiento automático. Puedes obtener esa vuelta gratis si usas un valor de auto tanto en la función de ancla como en la propiedad de inserción opuesta. Por ejemplo, si usas anchor para bottom, establece top en 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 las examinará hasta que encuentre una que pueda usar y, luego, aplicará ese posicionamiento. Si no puede encontrar uno que funcione, el valor predeterminado es el primero definido.

Un elemento position-fallback que intente mostrar la información sobre la herramienta anterior y luego debajo 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 lo aplicas a los cuadros 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 reutilizar el position-fallback para otros elementos. También puedes usar una propiedad personalizada con alcance para configurar anchor-default.

Considera usar esta demostración usando el barco de nuevo. Hay un conjunto de position-fallback. A medida que cambies la posición del ancla, la embarcación se ajustará para permanecer dentro del contenedor. Intenta cambiar también el valor de padding para ajustar el padding del cuerpo. Observa cómo el navegador corrige la posición. Se están modificando las posiciones cambiando la alineación de la cuadrícula del contenedor.

El elemento position-fallback es más detallado, esta vez, cuando intenta realizar 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 los anclas, veamos algunos ejemplos interesantes más allá de los cuadros de información. El objetivo de estos ejemplos es hacer que tus ideas fluyan para formas en las que podrías usar el posicionamiento de anclaje. La mejor manera de aprovechar las especificaciones más allá es con la información de usuarios reales como tú.

Menús contextuales

Comencemos con un menú contextual usando la API de Popover. La idea es que hacer clic en el botón con el corchete angular revelará un menú contextual. Ese menú tendrá su propio menú para expandir.

El lenguaje de marcado no es la parte importante aquí. Sin embargo, tienes tres botones cada uno con popovertarget. Entonces, tienes tres elementos que usan el atributo popover. Esto te permite abrir los menús contextuales sin JavaScript. Puede 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 elemento position-fallback y compartirlo entre los menús contextuales. También nos aseguramos de anular la configuración de los estilos de inset para los elementos emergentes.

[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 proporciona una IU de menú contextual anidado y adaptable. Intenta cambiar la posición del contenido con la función de selección. La opción que elijas actualiza la alineación de la cuadrícula. Esto afecta la forma en que el posicionamiento de los anclas posiciona las ventanas emergentes.

Enfocar y seguir

En esta demostración, se combinan las primitivas de CSS mediante la incorporación de :has(). La idea es realizar la transición de un indicador visual para el elemento input que está enfocado.

Para ello, configura una ancla nueva en el tiempo de ejecución. Para esta demostración, una propiedad personalizada con alcance se actualiza 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 aprovechar esto más? Podrías usarlo para una superposición instructiva. Un cuadro de información puede moverse entre los lugares de interés y actualizar su contenido. Podrías encadenar el contenido. Aquí, podrían funcionar las animaciones discretas que te permiten animar display o ver transiciones.

Cálculo de gráfico de barras

Otro aspecto divertido que puedes hacer con el posicionamiento de anclas es combinarlo con calc. Imagina un gráfico en el que tienes algunas ventanas emergentes que anotan el gráfico.

Puedes hacer un seguimiento de los valores más altos y más bajos mediante CSS min y max. El CSS podría ser similar al siguiente:

.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 código JavaScript en juego para actualizar los valores del gráfico y CSS para diseñar el gráfico. Sin embargo, el posicionamiento de anclas se encarga de las actualizaciones de diseño por nosotros.

Controladores de cambio de tamaño

No tienes que anclarlo a un solo elemento. Podrías usar muchas anclas para un elemento. Es posible que lo hayas notado en el ejemplo del gráfico de barras. Los cuadros de información se fijaron al gráfico y, luego, a la barra correspondiente. Si llevaste un poco más ese concepto, podrías usarlo para cambiar el tamaño de los elementos.

Puedes tratar los puntos de anclaje como controladores personalizados de cambio de tamaño y, también, aprovechar un valor de 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 arrastrables. 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 pregunta es un adelanto de lo que está por venir. Sin embargo, puedes crear un elemento emergente enfocable y ahora tendrás posicionamiento de ancla. Podrías crear los fundamentos de un elemento <select> con estilo.

<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á esta tarea. Sin embargo, para un punto de partida rudimentario, los CSS podrían tener el siguiente aspecto:

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

Combina las características de la API de Popover con el posicionamiento de CSS Anchor y estarás muy cerca.

Es útil cuando comiences a presentar elementos como :has(). Puedes rotar el marcador cuando esté abierto:

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

¿Qué puedes hacer a continuación? ¿Qué más necesitamos para que sea un select que funcione? Guardaremos eso para el siguiente artículo. Pero no te preocupes, ya habrá elementos de selección con estilo. ¡No te pierdas las novedades!


Eso es todo.

La plataforma web está evolucionando. El posicionamiento de las anclas de CSS es una parte fundamental para mejorar la forma en que desarrollas los controles de la IU. Esto te evitará algunas de esas decisiones complicadas. Pero también te permitirá hacer cosas que nunca antes habías hecho. Por ejemplo, aplicar diseño a un elemento <select>. Danos tu opinión.

Foto de CHUTTERSNAP en Unsplash