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

Como você faz o tethering de um elemento a outro? Tente rastrear as posições delas 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;
}

Muitas vezes, essas soluções não são ideais. Ele precisa de JavaScript ou introduzir marcação extra. A API de posicionamento de âncoras CSS tem como objetivo resolver isso fornecendo uma API CSS para elementos de tethering. Ela permite posicionar e dimensionar um elemento com base na posição e no tamanho dos outros.

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

Suporte ao navegador

Você pode testar a API de posicionamento de âncora do CSS no Chrome Canary com a flag "Recursos experimentais da plataforma Web". Para ativar essa flag, abra o Chrome Canary e acesse chrome://flags. Em seguida, ative a flag "Recursos da plataforma da Web experimentais".

Um polyfill também está em desenvolvimento pela equipe da Oddbird. Verifique 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 mais geral. A implementação atual também não está totalmente sincronizada com as especificações do grupo de trabalho CSS.

O problema

Por que você precisaria fazer isso? Um caso de uso importante seria a criação de dicas ou experiências semelhantes a dicas. Nesse caso, é recomendável vincular a dica ao conteúdo a que ela faz referência. Muitas vezes, há uma necessidade de vincular um elemento a outro. Você também espera que a interação com a página não quebra esse tethering, por exemplo, se um usuário rolar ou redimensionar a interface.

Outra parte do problema é garantir que o elemento vinculado permanece na visualização, por exemplo, se você abrir uma dica e ela for cortada pelos limites da janela de visualização. Essa pode não ser uma experiência muito boa para os usuários. Você quer que a dica se adapte.

Soluções atuais

Atualmente, existem algumas maneiras diferentes de abordar o problema.

O primeiro é a abordagem rudimentar "Encapsule a âncora". Você coloca 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%);
}
(em inglês)

É possível mover o contêiner e tudo vai ficar onde você quer a maior parte.

Outra abordagem é se você souber a posição da âncora ou acompanhá-la de alguma forma. Você pode passar isso para sua 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? Você provavelmente terá que 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 de CSS para 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 gerar algumas perguntas:

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

Isso resolve o problema? Isso pode ser para seu caso de uso, mas há um problema: nossa solução não se adapta. 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 crescer. Tudo o que você quer fazer é ancorar um elemento em outro. E 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 JavaScript para ajudá-lo. Isso gera o custo de adicionar uma dependência ao 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, é possível que suas perguntas e decisões não sejam reduzidas, mas sim alteradas. Isso faz parte do "porquê" do posicionamento de âncoras de CSS. Assim, você não precisa mais se preocupar com problemas de desempenho ao calcular a posição.

Veja como o código poderia ficar para 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 esperado. Ele reage ao sair da janela de visualização no eixo y, mas não no eixo x. Analise a documentação para encontrar uma solução que funcione para você.

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

Como usar o posicionamento de âncoras

Insira a API de posicionamento de âncoras 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.

  • Nenhum JavaScript é necessário.
  • Deixe o navegador determinar a melhor posição possível com sua orientação.
  • Sem dependências de terceiros
  • Nenhum elemento wrapper.
  • Funciona com elementos que estão na camada superior.

Vamos recriar e enfrentar o problema que estávamos tentando resolver acima. Mas, em vez disso, use a analogia de um barco com uma âncora. Eles representam a âncora e o elemento ancorado. 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 daked-ident.

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

Como alternativa, você poderá 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. Pode ser top, right, bottom, left, center etc. Também é possível transmitir uma porcentagem. Por exemplo, 50% seria 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. Também é possível usar a função anchor no 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;
}

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

.

Você deve ter notado o uso da propriedade personalizada --boat-size acima. Mas, se você quiser basear o tamanho do elemento ancorado na âncora, também pode acessar esse tamanho. Em vez de calcular por conta própria, você pode usar a função anchor-size. Por exemplo, para deixar 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). É possível usá-la para definir o tamanho de um dos eixos ou de ambos.

E se você quiser fixar um elemento com 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, você pode ancorar a ele.

<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 é possível arrastar a âncora e o barco seguirá.

Rastreando a posição de rolagem

Em alguns casos, seu elemento de â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, você precisa de uma maneira de rastreá-la. A propriedade anchor-scroll pode fazer isso. Você o define no elemento ancorado e fornece o valor da âncora que você deseja rastrear.

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

Teste esta demonstração em que é possível ativar e desativar o anchor-scroll com a caixa de seleção no canto.

.

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

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

.

No entanto, você vai perceber como os pop-ups rastreiam os respectivos links âncora. É possível 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 ancorado com base em um conjunto de substitutos fornecidos por você. Você guia o navegador com seus estilos e deixa que ele ajuste a posição para você.

O caso de uso comum aqui é uma dica que precisa alternar entre ser mostrado acima ou abaixo de uma âncora. Esse comportamento considera se a dica seria cortada pelo contêiner. Geralmente, esse contêiner é a janela de visualização.

Se você analisar o código da última demonstração, verá que há uma propriedade position-fallback em uso. Se você rolou o contêiner, pode ter notado que os popovers ancorados pularam. 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 o posicionamento automático. É possível fazer essa virada sem custo financeiro usando um valor de auto na função de â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 requer que você defina um conjunto de substituto de posição. O navegador irá por essas etapas até encontrar uma que possa usar e, em seguida, aplicará esse posicionamento. Se ele não conseguir 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 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 conjunto position-fallback. Quando você muda a posição da âncora, o barco é ajustado para permanecer no 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 alteração do alinhamento da grade do contêiner.

Desta vez, o position-fallback é mais detalhado mostrando as 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 do posicionamento de âncoras, vamos analisar alguns exemplos interessantes além das dicas. Esses exemplos visam fazer suas ideias fluírem para formas de usar o posicionamento de âncoras. A melhor maneira de levar as especificações adiante é 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 divisa revelará um menu de contexto. Esse menu terá seu próprio menu para expandir.

A marcação não é a parte importante aqui. No entanto, você tem três botões, cada um usando popovertarget. Então você tem três elementos que usam o atributo popover. Isso permite abrir os menus de contexto sem qualquer JavaScript. O resultado será semelhante a este:

<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 removemos todos os estilos 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 IU adaptável do menu de contexto aninhado. Tente mudar a posição do conteúdo com o recurso de seleção. A opção escolhida atualiza o alinhamento da grade. E isso afeta como o posicionamento da âncora posiciona os pop-ups.

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 em 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 levar isso adiante? Você pode usar esse recurso em algum tipo de sobreposição instrucional. Ela pode alternar entre pontos de interesse e atualizar o conteúdo dela. Você pode fazer o fading cruzado do conteúdo. Animações discretas que permitem animar display ou View Transitions podem funcionar aqui.

.

Cálculo do gráfico de barras

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

É possível rastrear os valores mais altos e mais baixos usando CSS min e max. O CSS para isso poderia 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 JavaScript em jogo para atualizar os valores do gráfico e algum CSS para estilizar o gráfico. No entanto, o posicionamento de âncoras cuida das atualizações do layout.

Redimensionar alças

Você não precisa fixar apenas um elemento. Você pode usar muitas âncoras para um elemento. Você deve ter notado isso no exemplo do gráfico de barras. As dicas estavam fixadas no gráfico e depois na barra apropriada. Se você levar esse conceito um pouco mais adiante, poderá usá-lo para redimensionar elementos.

Você pode 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 as alças arrastáveis. No entanto, o elemento <img> é redimensionado para preencher o contêiner que se ajusta e preencher a lacuna entre as alças.

.

Um SelectMenu?

Esta última é uma amostra do que está por vir. No entanto, você pode criar um pop-up focalizável e agora tem o posicionamento da â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 torna isso mais fácil. No entanto, o CSS para um ponto de partida rudimentar pode 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 recursos como o :has(). Você pode girar o marcador ao abrir:

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

O que fazer em seguida? O que mais precisamos para que essa select funcione? Vamos salvar isso para o próximo artigo. Mas não se preocupe, elementos de seleção estilizados estão chegando. Não perca as novidades!


Pronto!

A plataforma da Web está evoluindo. O posicionamento de âncoras de CSS é uma parte crucial para melhorar a forma como você desenvolve os controles de interface. Isso vai abstrair você de algumas dessas decisões complicadas. Mas também permitirá que você faça coisas que nunca foi capaz de fazer antes. Por exemplo, estilizar um elemento <select>. Envie sua opinião.

Foto de CHUTTERSNAP no Unsplash