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

Atualmente, como você faz o tethering de um elemento a outro? Você pode tentar rastrear as posições deles ou usar alguma forma 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;
}

Em geral, essas soluções não são ideais. Precisam de JavaScript ou introduzir marcação extra. A API de posicionamento de âncoras do CSS tem como objetivo resolver esse problema fornecendo uma API CSS para elementos de tethering. Ele fornece uma maneira 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

Você pode testar a API de posicionamento de âncoras de CSS no Chrome Canary com os "Recursos experimentais da plataforma Web" . Para ativar essa sinalização, abra o Chrome Canary e acesse chrome://flags. Depois, ative "Recursos experimentais da plataforma da Web". .

O Oddbird também tem um polyfill em desenvolvimento (em inglês). 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 em um nível elevado. A implementação atual também não está totalmente sincronizada com as especificações do grupo de trabalho do CSS (link em inglês).

O problema

Por que vocês precisam fazer isso? Um caso de uso de destaque seria a criação de dicas ou experiências semelhantes. Nesse caso, é recomendável vincular a dica ao conteúdo a que ela faz referência. Muitas vezes, é necessário estabelecer o vínculo entre um elemento e outro. Você também espera que interagir com a página não quebre esse vínculo, por exemplo, se um usuário rolar ou redimensionar a interface.

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

Soluções atuais

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

O primeiro é o rudimentar "Envolver a âncora" abordagem humilde. Você pega os dois elementos e os envolve em um contêiner. Em seguida, use position para posicionar a dica 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 permanecerá onde você quiser na maior parte do tempo.

Outra abordagem possível é se você souber a posição do seu anúncio âncora ou se puder acompanhá-lo de alguma forma. Você pode transmiti-la 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));
}
(link em inglês)

Mas e se você não souber a posição de sua âncora? É provável que você precise intervir no JavaScript. Você pode fazer algo como o código a seguir, mas agora 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 fazer algumas perguntas:

  • Quando calculo os estilos?
  • Como calculo os estilos?
  • Com que frequência calculo os estilos?

Isso resolve o problema? Pode ser para seu caso de uso, mas há um problema: nossa solução não se adapta. Ele não é responsivo. E se meu elemento ancorado for cortado pela janela de visualização?

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

Para aliviar um pouco desse problema, você pode recorrer a uma solução em JavaScript. Isso incorrerá no custo de adicionar uma dependência ao seu projeto e pode introduzir problemas de desempenho, dependendo de como você os 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. Como resultado, suas perguntas e decisões podem não ser reduzidas, mas alteradas. Isso é parte do “porquê” para o posicionamento de âncoras de CSS. Ele abstrai você de pensar em problemas de desempenho ao calcular a posição.

Veja como ficaria o código ao usar o floating-ui, um pacote bastante usado 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.

(link em inglês)

A "dica" podem não se comportar como esperado. Ele reage ao sair da janela de visualização no eixo Y, mas não no eixo x. Consulte a documentação para encontrar uma solução que funcione para você.

Mas encontrar um pacote que funcione para o seu projeto pode levar muito tempo. São decisões extras e podem ser frustrantes se não funcionarem como você quer.

Usar o posicionamento da âncora

Insira a API de posicionamento de âncoras de CSS. A ideia é manter seus 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 JavaScript.
  • Permita que o navegador determine a melhor posição com base na sua orientação.
  • Sem dependências de terceiros
  • Nenhum elemento wrapper.
  • Funciona com elementos da camada superior.

Vamos recriar e resolver o problema que estávamos tentando resolver acima. Mas 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.

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 traçado-ident.

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

Como alternativa, você pode 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, use a função anchor. A função anchor usa três argumentos:

  • Elemento de âncora:o anchor-name da âncora a ser usada. Você também pode omitir o valor para usar uma âncora implicit. Ela pode ser definida pela relação HTML ou com uma propriedade anchor-default com um valor anchor-name.
  • Lado da âncora:uma palavra-chave da posição que você quer usar. Os exemplos podem ser top, right, bottom, left, center etc. Também é possível transmitir uma porcentagem. Por exemplo, 50% é igual a center.
  • Substituto:é um valor substituto opcional que aceita uma duração ou porcentagem.

Use a função anchor como um valor para as propriedades de encarte (top, right, bottom, left ou equivalentes lógicos) do elemento ancorado. Você também pode 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 encarte center. Portanto, uma opção é usar calc se você souber o tamanho do elemento ancorado. Por que não usar translate? Você pode usar isto:

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

Porém, o navegador não leva em consideração posições transformadas para elementos ancorados. Vai ficar claro por que isso é importante ao considerar substitutos de posição e posicionamento automático.

(link em inglês)

Você deve ter notado o uso da propriedade personalizada --boat-size acima. No entanto, se você quiser basear o tamanho do elemento ancorado no tamanho da âncora, também poderá acessar esse tamanho. Em vez de fazer o cálculo por conta própria, você pode usar a função anchor-size. Por exemplo, para fazer nosso 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 dos eixos ou de ambos.

E se você quiser fixar um elemento com o posicionamento absolute? A regra é que os elementos não podem ser irmãos. Nesse caso, é possível unir a âncora com um contêiner que tenha o posicionamento relative. Depois, é só fixar.

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

Rastrear a posição de rolagem

Em alguns casos, o elemento âncora pode estar em um contêiner de rolagem. Ao mesmo tempo, o elemento ancorado pode ficar fora desse contêiner. Como a rolagem acontece em uma linha de execução diferente do layout, você precisa de uma maneira de acompanhá-la. A propriedade anchor-scroll pode fazer isso. Você o define no elemento ancorado e fornece o valor da âncora que quer acompanhar.

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

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

A analogia é um pouco achatada aqui, porém, como em um mundo ideal, seu barco e sua âncora estão ambos na água. Além disso, recursos como a API Popover promovem a capacidade de manter elementos relacionados próximos. O posicionamento da âncora funcionará com elementos que estão na camada superior. Esse é um dos principais benefícios da API: a possibilidade de vincular elementos em diferentes fluxos.

Considere esta demonstração que tem um contêiner de rolagem com âncoras e dicas. Os elementos de dica que são popovers podem não estar localizados com as âncoras:

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

Substituto de posição e posicionamento automático

É aqui que o poder de posicionamento da âncora sobe um nível. Uma position-fallback pode posicionar o elemento fixo com base em um conjunto de substitutos fornecidos. Você orienta o navegador com seus estilos e deixa que ele escolha a posição que preferir.

O caso de uso comum aqui é uma dica que precisa alternar entre a exibição acima ou abaixo de uma âncora. E esse comportamento é baseado no fato de a dica ser cortada pelo contêiner dela. Esse contêiner geralmente é a janela de visualização.

Se você analisasse o código da última demonstração, teria notado que havia uma propriedade position-fallback em uso. Se você rolou o contêiner, pode ter notado que os pop-ups ancorados saltaram. Isso acontecia quando as respectivas âncoras se aproximavam do limite da janela de visualização. 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 da âncora também oferece posicionamento automático. Você pode fazer essa virada sem custo financeiro usando um valor de auto na função âncora e na propriedade de encarte oposta. 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. Isso exige que você defina um conjunto de substituição de posição. O navegador vai passar por elas até encontrar uma que possa ser usada e, então, aplicar o posicionamento. Se ele não encontrar um que funcione, o padrão será o primeiro definido.

Uma position-fallback que tenta exibir 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 às dicas vai ficar 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 conjunto de position-fallback. À medida que você muda a posição da âncora, o barco se ajusta para permanecer dentro do contêiner. Tente também mudar o valor do padding, que ajusta o padding do corpo. Observe como o navegador corrige o posicionamento. As posições estão sendo alteradas com a mudança do alinhamento da grade do contêiner.

(link em inglês)

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ê tem uma ideia dos principais recursos para o posicionamento de âncoras, vamos analisar alguns exemplos interessantes além das dicas. Estes exemplos visam fazer com que suas ideias fluam sobre maneiras pelas quais você pode usar o posicionamento da âncora. A melhor maneira de ir mais além nas especificações é com a opinião de usuários reais como você.

Menus de contexto

Vamos começar com um menu de contexto usando a API Popover. A ideia é clicar no botão com o chevron para revelar um menu de contexto. E esse menu terá um menu próprio para ser expandido.

A marcação não é a parte importante aqui. No entanto, você tem três botões, cada um usando popovertarget. Em seguida, você terá três elementos usando o atributo popover. Isso permite que você abra os menus de contexto sem qualquer JavaScript. Ela poderia ficar 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 cancelamos a definição de qualquer estilo inset para os popovers.

[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 opção "Selecionar". A opção escolhida atualiza o alinhamento de grade. Isso afeta como o posicionamento da âncora posiciona os popovers.

(link em inglês)

Focar e seguir

Esta demonstração combina os primitivos do CSS trazendo :has(). A ideia é fazer a transição de um indicador visual para o input que está em foco.

Para isso, defina uma nova âncora no tempo de execução. Para esta 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á-los para alguma forma de sobreposição instrucional. Uma dica pode alternar entre pontos de interesse e atualizar o conteúdo dela. Você poderia fazer a transição para o conteúdo. Animações discretas que permitem a animação de display ou transições de visualização podem funcionar aqui.

Cálculo do gráfico de barras

Outra coisa divertida que pode ser feita com o posicionamento de âncoras é combiná-la com calc. Imagine um gráfico com alguns popovers que anotam o gráfico.

É possível acompanhar os valores mais altos e mais baixos usando CSS min e max. O CSS para isso pode ser parecido com este:

.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 alguns CSS para estilizar o gráfico. Mas o posicionamento da âncora cuida das atualizações do layout para nós.

(link em inglês)

Redimensionar alças

Não é necessário fixar um único elemento. É possível usar várias âncoras em um elemento. Você deve ter notado isso no exemplo do gráfico de barras. As dicas eram fixadas no gráfico e, em seguida, na barra adequada. Se você levar esse conceito um pouco mais longe, poderá usá-lo para redimensionar elementos.

É possível tratar os pontos de fixação 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 os identificadores arrastáveis. No entanto, o elemento <img> é redimensionado para preencher o contêiner que se ajusta para preencher a lacuna entre as alças.

Um SelectMenu?

Essa última é uma prévia do que está por vir. No entanto, você pode criar um pop-over focalizável e agora tem o posicionamento de âncora. Você pode criar as bases de um elemento <select> estilizado.

<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. Mas o CSS para um ponto de partida rudimentar poderia ser assim:

[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 CSS e pronto.

É ótimo quando você começa a apresentar coisas como o :has(). Você pode girar o marcador quando aberto:

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

Aonde você pode ir atrás disso? O que mais precisamos para tornar esse select funcional? 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á evoluindo. O posicionamento da âncora de CSS é uma parte crucial para melhorar o modo como você desenvolve os controles de IU. Isso vai abstrair você de algumas dessas decisões complicadas. Mas também permitirá que você faça coisas que nunca foi possível fazer antes. Por exemplo, definir o estilo de um elemento <select>. Envie sua opinião.

Foto de CHUTTERSNAP no Unsplash