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

¿Cómo conectas actualmente un elemento a otro? Puedes 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 ideales. Necesitan JavaScript o incorporan lenguaje de marcado adicional. El objetivo de la API de posicionamiento de anclajes de CSS es resolver esto proporcionando una API de CSS para conectar 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 otros elementos.

La imagen muestra una maqueta de la ventana 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 las “Funciones experimentales de la plataforma web” marca. Para habilitar esa marca, abre Chrome Canary y visita chrome://flags. A continuación, habilita las "Funciones experimentales de la plataforma web". marca.

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

Puedes verificar la compatibilidad de los anclajes 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 abordan las partes importantes a grandes rasgos. Además, la implementación actual no está completamente sincronizada con las especificaciones del grupo de trabajo de CSS.

El problema

¿Por qué necesitarías hacer esto? Un caso de uso destacado sería la creación de cuadros de información o experiencias similares a estos. En ese caso, es recomendable que vincules la información sobre la herramienta al contenido al que hace referencia. A menudo, se necesita alguna manera de conectar un elemento a otro. También esperas que interactuar con la página no provoque la conexión mediante dispositivo móvil, por ejemplo, si un usuario se desplaza por la IU o cambia su tamaño.

Otra parte del problema es que debes asegurarte de que el elemento anclado permanezca a la vista, por ejemplo, si abres un cuadro de información y se recorta por los límites del viewport. Es posible que esta no sea una gran experiencia para los usuarios. Quieres que se adapte la información sobre la herramienta.

Soluciones actuales

Actualmente, hay distintas formas de abordar el problema.

En primer lugar, está el rudimentario "Wrap the Anchor" (Envuelve el ancla) enfoque. Se toman ambos elementos y se los une 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 quedará donde quieras durante la mayor parte del tiempo.

Otro enfoque podría ser conocer la posición del ancla o hacer un seguimiento de ella. Puedes pasarla a la información sobre la herramienta 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 como lo hace el siguiente código, pero ahora significa que tus estilos están empezando a filtrarse de CSS y 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 plantea algunas preguntas:

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

¿Eso lo soluciona? 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 anclado?

Ahora debes decidir si reaccionar a esto y cómo hacerlo. La cantidad de preguntas y decisiones que debes tomar está empezando a crecer. Lo único que debes hacer es anclar un elemento a otro. En un mundo ideal, tu solución se ajustará y reaccionará a su entorno.

Para aliviar este problema, recomendamos que busques una solución de JavaScript que te ayude. Eso incurrirá en el costo de agregar una dependencia a tu proyecto y podría introducir 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 se modifiquen. Esto forma parte del porqué para el posicionamiento de los anclajes de CSS. No te distraerá de pensar en problemas de rendimiento cuando calcules la posición.

A continuación, se muestra cómo se vería el código si se usa "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.

El cuadro de información quizás no se comporte como esperas. Reacciona al salir de la viewport en el eje Y, pero no en el eje X. Profundiza en 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. Se trata de decisiones adicionales y puede ser frustrante si no funciona como quieres.

Cómo usar el posicionamiento de los anclajes

Ingresa la API de posicionamiento de anclajes 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 el objetivo es mejorar la experiencia del desarrollador.

  • No se requiere JavaScript.
  • Deja que el navegador determine la mejor posición según tus indicaciones.
  • No más dependencias de terceros
  • No hay elementos wrappers.
  • Funciona con los elementos que están en la capa superior.

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

Primero, debes elegir cómo definir el ancla. Puedes hacerlo en tu CSS si configuras la propiedad anchor-name en el elemento de anclaje. Acepta un valor de identidad punteada.

.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í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 anclaje que se usará. También puedes omitir el valor si quieres 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 fijo: Es una palabra clave de la posición que deseas usar. Puede ser top, right, bottom, left, center, etc. También puedes pasar un porcentaje. Por ejemplo, 50% sería igual a center.
  • Resguardo: Es un valor alternativo opcional que acepta una duración o un porcentaje.

Se 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 anclado. ¿Por qué no usar translate? Podrías usar lo siguiente:

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

Sin embargo, el navegador no considera las posiciones transformadas de 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 mencionada anteriormente. Pero, 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 que nuestro barco sea cuatro veces el ancho de la ancla:

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

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

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

<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 seguirá.

Seguimiento de la posición de desplazamiento

En algunos casos, el elemento de anclaje puede estar dentro de un contenedor de desplazamiento. Al mismo tiempo, tu elemento anclado 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 hacer esto. Se establece en el elemento anclado y se le asigna el valor del ancla a la que deseas hacer un seguimiento.

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

Prueba esta demostración, en la que puedes encender y apagar anchor-scroll con la casilla de verificación de la esquina.

Sin embargo, la analogía es un poco errada, como en un mundo ideal, el barco y el ancla están en el agua. Además, funciones como la API de Popover permiten mantener cerca los elementos relacionados. Sin embargo, el posicionamiento de los anclajes funcionará con los elementos que se encuentran en la capa superior. Este es uno de los principales beneficios de la API: poder anclar 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 información que son ventanas emergentes no se ubiquen junto a los anclas:

Sin embargo, observarás cómo las ventanas emergentes hacen un seguimiento de sus respectivos enlaces de anclaje. Puedes cambiar el tamaño de ese contenedor de desplazamiento y las posiciones se actualizarán por ti.

Posicionamiento automático y resguardo de posición

Aquí es donde el poder de posicionamiento de los anclas aumenta un 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 determine la posición por ti.

El caso de uso común es un cuadro de información que debe alternar entre mostrarse arriba o debajo de un ancla. Este comportamiento se basa en si el contenedor recorta la información sobre la herramienta. Por lo general, ese contenedor es el viewport.

Si revisaste el código de la última demostración, hubieras visto que había una propiedad position-fallback en uso. Si te desplazaste por el contenedor, es posible que hayas notado que esas ventanas emergentes ancladas saltaron. Esto ocurrió cuando sus respectivos anclas se acercaban al límite del viewport. En ese momento, las ventanas emergentes intentan ajustarse para permanecer en el viewport.

Antes de crear un position-fallback explícito, el posicionamiento de los anclajes también ofrecerá un posicionamiento automático. Para realizar ese giro de forma gratuita, usa un valor de auto en la función de anclaje y 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 los examinará hasta encontrar uno que pueda usar y luego aplicará ese posicionamiento. Si no encuentras uno que funcione, el valor predeterminado será el primero definido.

Una position-fallback que intente mostrar la información de arriba y a continuación 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.

Piensa en esta demostración usando el barco nuevamente. Hay un conjunto position-fallback. A medida que cambies la posición del ancla, el bote 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 el posicionamiento. Las posiciones se cambian cambiando la alineación de la cuadrícula del contenedor.

Esta vez, position-fallback es más detallado cuando intenta 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 anclajes, analicemos algunos ejemplos interesantes más allá de la información sobre la herramienta. El objetivo de estos ejemplos es hacer fluir tus ideas para encontrar formas en las que podrías utilizar el posicionamiento de anclaje. La mejor manera de llevar la especificación al siguiente nivel es con aportes de usuarios reales como tú.

Menús contextuales

Comencemos con un menú contextual que use la API de Popover. La idea es que, al hacer clic en el botón con la comilla angular, se abrirá un menú contextual. Y 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. Luego, tienes tres elementos que usan el atributo popover. Eso te permite abrir los menús contextuales sin JavaScript. Se podría ver 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 quitar los estilos de inset para las ventanas 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 brinda una IU de menú contextual anidado adaptable. Intenta cambiar la posición del contenido con la selección. La opción que elijas actualiza la alineación de la cuadrícula. Y eso afecta cómo el posicionamiento de los anclas posiciona las ventanas emergentes.

Enfocar y seguir

En esta demostración, se combinan las primitivas de CSS incorporando :has(). La idea es realizar una transición de un indicador visual para el elemento input enfocado.

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

Cálculo de gráfico de barras

Otra cosa divertida que puedes hacer con el posicionamiento de los anclajes 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 con las funciones 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 algo de JavaScript en juego para actualizar los valores del gráfico y algo de CSS para darle estilo al gráfico. Pero el posicionamiento de anclas se encarga de las actualizaciones del diseño por nosotros.

Controladores de cambio de tamaño

No tienes que anclar a un solo elemento. Podrías usar muchos anclas para un elemento. Es posible que hayas notado eso en el ejemplo del gráfico de barras. Los cuadros de información se fijaron en el gráfico y, luego, en la barra correspondiente. Si hubieras ampliado ese concepto, 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 utilizar 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 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 viene. Sin embargo, puedes crear una ventana emergente enfocable y ahora tendrás el posicionamiento del ancla. Puedes 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 lo facilitará. Sin embargo, el CSS para un punto de partida rudimentario podría verse así:

[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 los anclajes de CSS y estarás cerca.

Es interesante comenzar a introducir elementos como :has(). Puedes rotar el marcador cuando lo abras:

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

¿Qué harías a continuación? ¿Qué más necesitamos para que sea un select funcional? Guardaremos eso para el próximo artículo. Pero no te preocupes, ya que pronto habrá elementos de selección con estilo. ¡No te pierdas ninguna novedad!


Eso es todo.

La plataforma web está evolucionando. El posicionamiento de los anclajes de CSS es fundamental para mejorar el desarrollo de los controles de IU. Esto te apartará de algunas de esas decisiones complicadas. Pero también te permitirá hacer cosas que antes no habías podido hacer. Por ejemplo, asignarle un estilo a un elemento <select>. Danos tu opinión.

Foto de CHUTTERSNAP en Unsplash