Vincular elementos entre si com o posicionamento de âncoras de CSS

Como você conecta um elemento a outro? Você pode tentar rastrear as posições ou usar algum 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;
}

Essas soluções geralmente não são ideais. Eles precisam de JavaScript ou de uma marcação extra. A API de posicionamento de âncora CSS tem como objetivo resolver esse problema, fornecendo uma API CSS para elementos de tethering. Ele fornece um meio de posicionar e dimensionar um elemento com base na posição e no tamanho de outros elementos.

A imagem mostra uma janela de modelo do navegador detalhando a anatomia de uma dica.

Suporte ao navegador

É possível testar a API de posicionamento de âncora CSS no Chrome Canary com a flag "Experimental Web Platform Features". Para ativar essa flag, abra o Chrome Canary e acesse chrome://flags. Em seguida, ative a flag "Recursos experimentais da plataforma da Web".

Há também um polyfill em desenvolvimento pela equipe da Oddbird. Confira o repositório em github.com/oddbird/css-anchor-positioning.

É possível verificar o suporte à ancoragem com:

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

Essa API ainda está em fase experimental e pode mudar. Este artigo aborda as partes importantes de forma geral. A implementação atual também não está totalmente sincronizada com a especificação do Grupo de trabalho do CSS.

O problema

Por que você precisa fazer isso? Um caso de uso importante seria criar dicas ou experiências semelhantes a elas. Nesse caso, você geralmente quer vincular a dica de ferramenta ao conteúdo a que ela se refere. Muitas vezes, é necessário amarrar um elemento a outro. Você também espera que a interação com a página não quebre essa vinculação, por exemplo, se um usuário rolar ou redimensionar a interface.

Outra parte do problema é se você quiser garantir que o elemento vinculado permaneça visível. Por exemplo, se você abrir uma dica e ela for cortada pelos limites da viewport. Isso pode não ser uma ótima experiência para os usuários. Você quer que a dica de ferramenta se adapte.

Soluções atuais

No momento, há algumas maneiras diferentes de abordar o problema.

Primeiro, vamos abordar a abordagem rudimentar de "envolver a âncora". Você pega os dois elementos e os envolve em um contêiner. Em seguida, use position para posicionar a dica de ferramenta em relação à âncora.

<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%);
}

Você pode mover o contêiner, e tudo vai permanecer onde você quiser, na maior parte.

Outra abordagem é se você souber a posição da âncora ou puder rastreá-la de alguma forma. Você pode transmiti-lo para a dica com propriedades 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));
}

Mas e se você não souber a posição da âncora? Provavelmente, você vai precisar intervir com o JavaScript. Você pode fazer algo como o código a seguir, mas isso significa que seus estilos estão começando a vazar do CSS para o 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);

Isso começa a levantar algumas questões:

  • Quando devo calcular os estilos?
  • Como calcular os estilos?
  • Com que frequência preciso calcular os estilos?

Isso resolveu o problema? Talvez para seu caso de uso, mas há um problema: nossa solução não se adapta. Ele não responde. E se o elemento ancorado for cortado pela viewport?

Agora você precisa decidir se vai reagir a isso e como. O número de perguntas e decisões que você precisa tomar está aumentando. Você só quer ancorar um elemento a outro. E, em um mundo ideal, sua solução vai se ajustar e reagir ao ambiente.

Para facilitar um pouco, você pode usar uma solução em JavaScript. Isso vai gerar o custo de adicionar uma dependência ao projeto e pode causar problemas de desempenho, dependendo de como você as usa. Por exemplo, alguns pacotes usam requestAnimationFrame para manter a posição correta. Isso significa que você e sua equipe precisam se familiarizar com o pacote e as opções de configuração dele. Como resultado, suas perguntas e decisões podem não ser reduzidas, mas alteradas. Isso faz parte do "por que" do posicionamento de âncora do CSS. Isso vai evitar que você pense em problemas de desempenho ao calcular a posição.

Confira como o código pode ficar ao usar "floating-ui", um pacote conhecido para esse 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);

Tente reposicionar a âncora nesta demonstração que usa esse código.

A "dica" pode não se comportar como você espera. Ele reage ao sair da viewport no eixo y, mas não no eixo x. Leia a documentação e provavelmente vai encontrar uma solução que funcione para você.

No entanto, encontrar um pacote que funcione para seu projeto pode levar muito tempo. É uma decisão extra e pode ser frustrante se não funcionar como você quer.

Como usar o posicionamento de âncora

Insira a API de posicionamento de âncora do CSS. A ideia é manter os estilos no CSS e reduzir o número de decisões que você precisa tomar. Você espera alcançar o mesmo resultado, mas o objetivo é melhorar a experiência do desenvolvedor.

  • Não é necessário usar JavaScript.
  • Deixe o navegador encontrar a melhor posição com base na sua orientação.
  • Não há mais dependências de terceiros
  • Sem elementos de wrapper.
  • Funciona com elementos que estão na camada superior.

Vamos recriar e resolver o problema que estávamos tentando resolver acima. Em vez disso, use a analogia de um barco com uma âncora. Eles representam o elemento ancorado e a âncora. A água representa o bloco que contém o objeto.

Primeiro, você precisa escolher como definir a âncora. Para fazer isso no CSS, defina a propriedade anchor-name no elemento âncora. Ele aceita um valor dashed-ident.

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

Como alternativa, é possível definir uma âncora no HTML com o atributo anchor. O valor do atributo é o ID do elemento âncora. Isso cria uma âncora implícita.

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

Depois de definir uma âncora, você pode usar a função anchor. A função anchor usa três argumentos:

  • Elemento âncora:o anchor-name da âncora a ser usada. Você também pode omitir o valor para usar uma âncora implicit. Ele pode ser definido pela relação HTML ou com uma propriedade anchor-default com um valor anchor-name.
  • Lado do âncora: uma palavra-chave da posição que você quer usar. Pode ser top, right, bottom, left, center etc. Ou você pode transmitir uma porcentagem. Por exemplo, 50% seria igual a center.
  • Padrão:é um valor de fallback opcional que aceita um comprimento ou uma porcentagem.

Use a função anchor como um valor para as propriedades de recuo (top, right, bottom, left ou equivalentes lógicos) do elemento ancorado. Também é possível usar a função anchor em 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));
}

Não há uma propriedade de inserção center. Uma opção é usar calc se você souber o tamanho do elemento ancorado. Por que não usar translate? Você pode usar:

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

No entanto, o navegador não considera as posições transformadas para elementos ancorados. Vai ficar claro por que isso é importante ao considerar posições de fallback e posicionamento automático.

Você pode ter notado o uso da propriedade personalizada --boat-size acima. No entanto, se você quiser basear o tamanho do elemento ancorado no da âncora, também poderá acessar esse tamanho. Em vez de calcular por conta própria, use a função anchor-size. Por exemplo, para fazer o barco quatro vezes a largura da âncora:

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

Você também tem acesso à altura com anchor-size(--my-anchor height). E você pode usá-lo para definir o tamanho de um ou de ambos os eixos.

E se você quiser ancorar em um elemento com posicionamento absolute? A regra é que os elementos não podem ser irmãos. Nesse caso, é possível envolver a âncora com um contêiner que tenha posicionamento relative. Em seguida, você pode ancorar nele.

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

Confira esta demonstração em que você pode arrastar a âncora e o barco vai seguir.

Rastrear a posição de rolagem

Em alguns casos, o elemento âncora pode estar dentro de um contêiner de rolagem. Ao mesmo tempo, o elemento ancorado pode estar fora desse contêiner. Como a rolagem acontece em uma linha de execução diferente do layout, é necessário ter uma maneira de acompanhar isso. A propriedade anchor-scroll pode fazer isso. Você define o elemento ancorado e atribui a ele o valor da âncora que quer acompanhar.

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

Teste esta demonstração em que você pode ativar e desativar anchor-scroll com a caixa de seleção no canto.

A analogia não é perfeita, porque em um mundo ideal, o barco e a âncora estão na água. Além disso, recursos como a API Popover permitem manter elementos relacionados próximos. O posicionamento da âncora funciona com elementos que estão na camada de cima. Esse é um dos principais benefícios da API: poder vincular elementos em fluxos diferentes.

Considere esta demonstração que tem um contêiner de rolagem com âncoras que têm dicas de ferramentas. Os elementos de dica de ferramentas que são pop-ups não podem ser localizados com as âncoras:

No entanto, você vai notar como os pop-ups rastreiam os respectivos links de âncora. Você pode redimensionar esse contêiner de rolagem, e as posições serão atualizadas.

Posicionamento substituto e automático

É aqui que o poder de posicionamento de âncora aumenta. Um position-fallback pode posicionar o elemento ancorado com base em um conjunto de substitutos fornecidos. Você guia o navegador com seus estilos e deixa que ele resolva a posição.

O caso de uso comum aqui é uma dica que precisa alternar entre aparecer acima ou abaixo de uma âncora. Esse comportamento é baseado na possibilidade de a dica ser cortada pelo contêiner. Esse contêiner geralmente é a janela de visualização.

Se você analisou o código da última demonstração, deve ter notado que havia uma propriedade position-fallback em uso. Se você rolou o contêiner, talvez tenha notado que os pop-ups fixados pularam. Isso aconteceu quando as respectivas âncoras se aproximaram do limite da viewport. Nesse momento, os pop-ups estão tentando se ajustar para permanecer na janela de visualização.

Antes de criar um position-fallback explícito, o posicionamento de âncora também oferece o posicionamento automático. Você pode fazer essa inversão sem custo financeiro usando um valor de auto na função de âncora e na propriedade de inseto oposto. Por exemplo, se você usar anchor para bottom, defina top como auto.

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

A alternativa ao posicionamento automático é usar um position-fallback explícito. Para isso, você precisa definir um conjunto de substituição de posição. O navegador vai passar por elas até encontrar uma que possa usar e, em seguida, aplicar essa posição. Se não encontrar uma que funcione, o padrão será o primeiro definido.

Um position-fallback que tenta mostrar as dicas acima e abaixo pode ter esta aparência:

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

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

A aplicação disso nas dicas de ferramentas fica assim:

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

O uso de anchor-default significa que você pode reutilizar o position-fallback para outros elementos. Também é possível usar uma propriedade personalizada com escopo para definir anchor-default.

Considere esta demonstração usando o barco novamente. Há um position-fallback definido. À medida que você muda a posição da âncora, o barco se ajusta para permanecer dentro do contêiner. Tente mudar o valor do padding, que ajusta o padding do corpo. Observe como o navegador corrige o posicionamento. As posições são alteradas ao mudar o alinhamento da grade do contêiner.

O position-fallback é mais detalhado desta vez, tentando posições no sentido horário.

.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);
  }
}


Exemplos

Agora que você já tem uma ideia dos principais recursos para posicionamento de âncora, vamos conferir alguns exemplos interessantes além das dicas. O objetivo desses exemplos é ajudar você a ter ideias de como usar o posicionamento de âncora. A melhor maneira de melhorar a especificação é com a contribuição de usuários reais como você.

Menus de contexto

Vamos começar com um menu de contexto usando a API Popover. A ideia é que clicar no botão com a seta revela um menu de contexto. E esse menu terá um menu próprio para expandir.

A marcação não é a parte importante aqui. No entanto, você tem três botões usando popovertarget. Em seguida, você tem três elementos usando o atributo popover. Isso permite abrir os menus de contexto sem JavaScript. Ela pode ser assim:

<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>

Agora, você pode definir um position-fallback e compartilhá-lo entre os menus de contexto. Também desativamos todos os estilos inset para os 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);
  }
}

Isso fornece uma interface de menu de contexto aninhado adaptável. Tente mudar a posição do conteúdo com a seleção. A opção escolhida atualiza o alinhamento da grade. Isso afeta a forma como o posicionamento de âncora posiciona os popovers.

Foco e acompanhamento

Esta demonstração combina primitivas CSS com :has(). A ideia é fazer a transição de um indicador visual para o input com foco.

Para fazer isso, defina uma nova âncora no momento da execução. Nesta demonstração, uma propriedade personalizada com escopo é atualizada no foco 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;
}

Mas como você pode ir além? Você pode usá-la para alguma forma de sobreposição instrutiva. Uma dica pode se mover entre pontos de interesse e atualizar o conteúdo. Você pode fazer um crossfade do conteúdo. Animações discretas que permitem animar display ou transições de visualização podem funcionar aqui.

Cálculo de gráfico de barras

Outra coisa divertida que você pode fazer com o posicionamento de âncora é combiná-lo com calc. Imagine um gráfico com alguns popovers que fazem anotações nele.

É possível acompanhar os valores mais altos e mais baixos usando o CSS min e max. O CSS para isso pode ficar assim:

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

Há um pouco de JavaScript em jogo para atualizar os valores do gráfico e um pouco de CSS para estilizar o gráfico. Mas o posicionamento de âncora cuida das atualizações de layout.

Alças de redimensionamento

Não é necessário ancorar em apenas um elemento. É possível usar muitas âncoras para um elemento. Você pode ter notado isso no exemplo de gráfico de barras. As dicas de ferramentas foram ancoradas ao gráfico e à barra correspondente. Se você levar esse conceito um pouco mais adiante, poderá usá-lo para redimensionar elementos.

Você pode tratar os pontos de ancoragem como alças de redimensionamento personalizadas e usar um valor inset.

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

Nesta demonstração, o GreenSock Draggable torna as alças Draggable. No entanto, o elemento <img> é redimensionado para preencher o contêiner, que se ajusta para preencher a lacuna entre as alças.

Um SelectMenu?

Este último é uma provocação do que está por vir. No entanto, é possível criar um popover com foco e agora você tem o posicionamento de âncora. Você pode criar as bases de um elemento <select> estilizável.

<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>

Um anchor implícito facilita isso. No entanto, o CSS para um ponto de partida rudimentar pode ser parecido com este:

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

Combine os recursos da API Popover com o posicionamento de âncora do CSS e você estará quase lá.

É legal quando você começa a introduzir coisas como :has(). Você pode girar o marcador ao abrir:

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

Para onde você pode levá-lo em seguida? O que mais precisamos para que o select funcione? Vamos deixar isso para o próximo artigo. Mas não se preocupe, os elementos de seleção estilizáveis estão chegando. Não perca!


Pronto.

A plataforma da Web está em evolução. O posicionamento de âncora do CSS é uma parte crucial para melhorar a forma como você desenvolve controles de interface. Ele vai ajudar você a se afastar de algumas decisões complicadas. Mas também permite que você faça coisas que nunca fez antes. como estilizar um elemento <select>. Envie sua opinião.

Foto de CHUTTERSNAP no Unsplash