Back

CSS Anchor Positioning: O Fim das Bibliotecas JavaScript pra Tooltips (Guia Completo)

Todo dev frontend já travou a batalha do posicionamento de tooltips. Você precisa de um dropdown grudado num botão, um tooltip que segue o trigger, ou um popover preso a um elemento específico. A solução sempre foi a mesma: instalar uma biblioteca JavaScript, calcular coordenadas em cada scroll e resize, tratar detecção de overflow, e torcer pro z-index não quebrar tudo.

Floating UI (o sucessor do Popper.js) tem mais de 23 milhões de downloads semanais no npm só pro pacote core. São milhões de projetos compensando algo que o CSS não fazia nativamente — até agora.

A API CSS Anchor Positioning atingiu Baseline 2026 com suporte completo no Chrome 125+, Firefox 147+ e Safari 26. Não é feature experimental atrás de flag. Tá pronta pra produção, e muda fundamentalmente como a gente constrói elementos UI posicionados na web.

Esse guia cobre tudo: a API principal, estratégias avançadas de fallback, integração com a Popover API, padrões de migração de bibliotecas JavaScript, e os edge cases que vão te morder se você não tiver preparado.


O Problema que CSS Anchor Positioning Resolve

Antes dessa API existir, posicionar um elemento relativo a outro exigia uma de três abordagens:

1. Posicionamento Absoluto com Offsets Manuais — Frágil, quebra no scroll, não trata overflow.

/* O hack clássico */ .tooltip-wrapper { position: relative; } .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); }

Funciona pra layouts estáticos, mas desmorona quando o elemento trigger tá perto da borda do viewport, a página rola, ou o container tem overflow: hidden.

2. Cálculo de Posição com JavaScript — O padrão da indústria por uma década.

// O que o Floating UI faz por baixo dos panos (simplificado) function updatePosition(anchor, floating) { const anchorRect = anchor.getBoundingClientRect(); const floatingRect = floating.getBoundingClientRect(); let top = anchorRect.bottom + 8; let left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2; // Detecção de overflow if (top + floatingRect.height > window.innerHeight) { top = anchorRect.top - floatingRect.height - 8; // vira pra cima } if (left < 0) left = 8; // empurra pra direita if (left + floatingRect.width > window.innerWidth) { left = window.innerWidth - floatingRect.width - 8; // empurra pra esquerda } floating.style.top = `${top}px`; floating.style.left = `${left}px`; } // Precisa rodar em cada scroll, resize e mutação do DOM window.addEventListener('scroll', () => updatePosition(anchor, floating), { passive: true }); window.addEventListener('resize', () => updatePosition(anchor, floating)); const observer = new ResizeObserver(() => updatePosition(anchor, floating)); observer.observe(anchor);

Funciona, mas você tá rodando cálculos de layout em JavaScript a cada frame de scroll. Tá brigando com o pipeline de renderização do navegador em vez de trabalhar junto com ele.

3. CSS Anchor Positioning — A solução nativa.

.trigger { anchor-name: --my-trigger; } .tooltip { position: fixed; position-anchor: --my-trigger; bottom: anchor(top); left: anchor(center); translate: -50% 0; /* Tratamento automático de overflow */ position-try-fallbacks: flip-block; }

Sem JavaScript. Sem listeners de scroll. Sem resize observers. Sem getBoundingClientRect(). O navegador cuida de tudo dentro do pipeline de renderização, onde deveria estar desde o começo.


API Principal: Os Três Blocos Fundamentais

CSS Anchor Positioning se baseia em três conceitos: declarar anchors, posicionar contra anchors e tratar overflow.

1. Declarando um Anchor

Qualquer elemento vira um anchor se você der um nome pra ele com a propriedade anchor-name:

.profile-avatar { anchor-name: --avatar; } .settings-gear { anchor-name: --settings-btn; }

Regras:

  • Nomes usam a sintaxe CSS dashed-ident (prefixo --).
  • Um elemento só pode ter um anchor name por vez.
  • Anchor names existem no mesmo scope que o elemento posicionado — seguem as regras do containing block.

Dá pra declarar inline no HTML também:

<button style="anchor-name: --menu-trigger">Menu</button>

2. Posicionando Contra um Anchor

Pra posicionar um elemento relativo a um anchor, você precisa de três coisas:

  1. O elemento posicionado precisa ter position: absolute ou position: fixed.
  2. Vincule ele ao anchor via position-anchor.
  3. Use a função anchor() nas propriedades inset (top, right, bottom, left).
.status-badge { position: fixed; position-anchor: --avatar; /* Posiciona no canto inferior direito do avatar */ top: anchor(bottom); left: anchor(right); }

A função anchor() recebe um keyword de lado que aponta pra geometria do anchor:

Valor de anchor()Significado
anchor(top)A borda superior do anchor
anchor(bottom)A borda inferior do anchor
anchor(left)A borda esquerda do anchor
anchor(right)A borda direita do anchor
anchor(center)O ponto central do anchor (no eixo relevante)
anchor(start)Início lógico (respeita direção de escrita)
anchor(end)Fim lógico

Também rola usar anchor() com porcentagem:

.tooltip { position: fixed; position-anchor: --trigger; /* Posiciona a 25% da borda esquerda do anchor */ left: anchor(25%); bottom: anchor(top); }

O Atalho position-area

Pra posições comuns, position-area oferece um modelo mental mais simples. Em vez de pensar em lados individuais, pense numa grade 3×3 ao redor do anchor:

┌──────────┬──────────┬──────────┐
│ top left │ top      │ top right│
├──────────┼──────────┼──────────┤
│ left     │ center   │ right    │
├──────────┼──────────┼──────────┤
│ bottom   │ bottom   │ bottom   │
│ left     │          │ right    │
└──────────┴──────────┴──────────┘
/* Tooltip abaixo do anchor, centralizado */ .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; } /* Sidebar à direita do anchor */ .sidebar { position: fixed; position-anchor: --panel; position-area: right; } /* Badge de notificação no canto superior direito */ .badge { position: absolute; position-anchor: --icon; position-area: top right; }

Na maioria dos casos, position-area dá conta. Use as funções anchor() só quando precisar de precisão sub-elemento (tipo "20% da borda esquerda do anchor").

3. Tratamento de Overflow com position-try-fallbacks

Aqui é onde CSS Anchor Positioning passa por cima das bibliotecas JavaScript. position-try-fallbacks diz pro navegador o que fazer quando o elemento posicionado vai transbordar do containing block:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* Se transbordar embaixo, vira pra cima */ position-try-fallbacks: flip-block; }

Estratégias built-in:

EstratégiaComportamento
flip-blockVira pro lado oposto no eixo block (cima ↔ baixo)
flip-inlineVira no eixo inline (esquerda ↔ direita)
flip-block flip-inlineTenta virar nos dois eixos

Pra mais controle, defina posições fallback customizadas com @position-try:

@position-try --above { position-area: top center; margin-bottom: 8px; } @position-try --left-side { position-area: left; margin-right: 8px; } @position-try --right-side { position-area: right; margin-left: 8px; } .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* Tenta cada fallback na ordem até um caber */ position-try-fallbacks: --above, --left-side, --right-side; }

O navegador avalia cada fallback na ordem e escolhe o primeiro que não causa overflow. Acontece automaticamente durante o layout — sem JavaScript, sem requestAnimationFrame, sem resize observers.


Integração com a Popover API

CSS Anchor Positioning fica ainda mais forte combinado com a Popover API do HTML (atributo popover). Juntos, entregam a solução completa de tooltip/dropdown:

<button popovertarget="user-menu" style="anchor-name: --menu-btn"> Configurações ⚙️ </button> <div id="user-menu" popover anchor="menu-btn"> <nav> <a href="/profile">Perfil</a> <a href="/settings">Configurações</a> <a href="/logout">Sair</a> </nav> </div>
[popover] { position: fixed; position-anchor: --menu-btn; position-area: bottom left; margin-top: 4px; position-try-fallbacks: flip-block; }

O que você ganha de graça:

  • Renderização no top layer — Sem guerras de z-index. Popovers renderizam no top layer do navegador.
  • Light dismiss — Clique fora fecha automaticamente.
  • Acessível por padrão — Navegação por teclado e gerenciamento de foco pelo navegador.
  • Anchor positioning — O menu fica grudado no trigger, trata overflow, e ajusta no scroll.

Zero JavaScript. Tudo isso.

Padrão Tooltip com Popover

<button popovertarget="tip" popovertargetaction="toggle" style="anchor-name: --help-btn"> Ajuda? </button> <div id="tip" popover="hint"> Essa ação não pode ser desfeita. </div>
#tip { position: fixed; position-anchor: --help-btn; position-area: top center; margin-bottom: 8px; position-try-fallbacks: flip-block, flip-inline; background: var(--surface-inverse); color: var(--text-inverse); padding: 6px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 240px; }

A variante popover="hint" é não-modal e não rouba o foco — perfeita pra tooltips.

Nota: popover="hint" faz parte do foco do Interop 2026 e o suporte dos navegadores ainda tá crescendo. Se compatibilidade cross-browser for crítica, use popover="manual" com lógica JavaScript de show/hide como fallback.


Padrões do Mundo Real

Padrão 1: Dropdown com Submenus

.nav-item { anchor-name: --nav-item; } .dropdown { position: fixed; position-anchor: --nav-item; position-area: bottom span-right; margin-top: 4px; position-try-fallbacks: flip-block; } .dropdown-item { anchor-name: --sub-trigger; } .submenu { position: fixed; position-anchor: --sub-trigger; position-area: right; margin-left: 2px; position-try-fallbacks: flip-inline; }

Repara no keyword span-right no position-area — ele estica o elemento posicionado da posição do anchor pra direita, exatamente como um dropdown deve se comportar. Submenus usam flip-inline pra virar da direita pra esquerda quando transbordariam do viewport.

Padrão 2: Tooltip de Validação de Formulário

<div class="field"> <label for="email">Email</label> <input id="email" type="email" style="anchor-name: --email-input" required placeholder="[email protected]"> <div class="validation-msg" popover="hint" id="email-error"> Digite um endereço de email válido </div> </div>
#email-error { position: fixed; position-anchor: --email-input; position-area: right; margin-left: 12px; position-try-fallbacks: --below-field; background: var(--color-danger-surface); color: var(--color-danger-text); border: 1px solid var(--color-danger-border); padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 200px; } @position-try --below-field { position-area: bottom span-right; margin-top: 4px; margin-left: 0; }

No desktop, a mensagem de validação aparece à direita do input. No mobile ou viewports estreitos onde não tem espaço, automaticamente cai embaixo do campo. Sem media queries.

Padrão 3: Anotações Contextuais (Estilo Google Docs)

.comment-marker { anchor-name: --comment; background: var(--highlight-yellow); } .comment-thread { position: fixed; position-anchor: --comment; position-area: right; margin-left: 24px; width: 280px; position-try-fallbacks: --left-side; } @position-try --left-side { position-area: left; margin-right: 24px; }

Migrando de Bibliotecas JavaScript

Se você usa Floating UI, Tippy.js ou similar, aqui tá como os conceitos se mapeiam:

Floating UI → CSS Anchor Positioning

Conceito Floating UIEquivalente CSS
computePosition()position-anchor + anchor()
placement: 'bottom'position-area: bottom center
flip() middlewareposition-try-fallbacks: flip-block
shift() middlewareposition-try-fallbacks com @position-try custom
offset(8)margin-top: 8px (ou a margin correspondente)
autoUpdate()Built-in (automático)
arrow middlewarePseudo-elemento ::before com anchor()

Exemplo de Migração

Antes (Floating UI):

import { computePosition, flip, shift, offset } from '@floating-ui/dom'; const button = document.querySelector('#trigger'); const tooltip = document.querySelector('#tooltip'); function update() { computePosition(button, tooltip, { placement: 'bottom', middleware: [offset(8), flip(), shift({ padding: 5 })], }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); }); } const cleanup = autoUpdate(button, tooltip, update);

Depois (CSS Anchor Positioning):

<button style="anchor-name: --trigger">Passa o mouse aqui</button> <div class="tooltip">Conteúdo do tooltip</div>
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; position-try-fallbacks: flip-block, flip-inline; }

A versão JavaScript te obriga a importar uma biblioteca (~3KB min+gzip pro core do Floating UI), configurar listeners de auto-update e limpar no unmount. A versão CSS? Três propriedades e zero JavaScript.

E a Seta?

Bibliotecas JavaScript de tooltip normalmente fornecem um middleware de arrow. Com CSS Anchor Positioning, você usa um pseudo-elemento:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 12px; position-try-fallbacks: flip-block; } .tooltip::before { content: ''; position: absolute; bottom: 100%; left: anchor(--trigger center); translate: -50% 0; border: 6px solid transparent; border-bottom-color: var(--tooltip-bg); }

O pulo do gato: o pseudo-elemento da seta usa anchor() pra manter alinhamento com o centro do trigger, mesmo se o tooltip mudar de posição por causa do tratamento de overflow. Uma propriedade CSS onde antes precisava de middleware dedicado.


Edge Cases e Pegadinhas

1. Scope e Visibilidade do Anchor

Anchors precisam estar no mesmo scope de containing block que o elemento posicionado. Se seu anchor tá dentro de um container com overflow: hidden, o elemento posicionado não vai conseguir "ver" fora dele.

Solução: Use position: fixed no elemento posicionado.

2. Múltiplos Elementos com Mesmo Anchor Name

Se múltiplos elementos têm o mesmo anchor-name, só o último na ordem do DOM vira o anchor ativo.

Solução: Use nomes de anchor únicos.

3. Anchoring Implícito com o Atributo anchor

<button id="my-btn">Clique</button> <div anchor="my-btn" class="popup">Conteúdo</div>
.popup { position: fixed; position-area: bottom center; }

Atenção: o atributo HTML anchor usa o ID do elemento (sem --), enquanto anchor-name no CSS usa o dashed-ident (com --). Não misture.

4. Considerações de Animação

Pra animar tooltips ancorados, use @starting-style pra definir o estado inicial da transição:

.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; opacity: 0; transform: translateY(-4px); transition: opacity 0.2s, transform 0.2s; } .tooltip:popover-open { opacity: 1; transform: translateY(0); } @starting-style { .tooltip:popover-open { opacity: 0; transform: translateY(-4px); } }

5. Troca Dinâmica de Anchor

Você pode mudar dinamicamente contra qual anchor um elemento se posiciona:

.contextual-toolbar { position: fixed; position-anchor: --active-selection; position-area: top center; margin-bottom: 8px; }
document.addEventListener('selectionchange', () => { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const marker = document.getElementById('selection-marker'); const rect = range.getBoundingClientRect(); marker.style.cssText = ` anchor-name: --active-selection; position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px; pointer-events: none; `; } });

Útil pra toolbars contextuais estilo Medium onde a posição do anchor muda conforme a interação.


Performance: Por Que Nativo Ganha do JavaScript

A diferença de performance não é marginal — é de arquitetura.

Pipeline de Posicionamento JavaScript

Usuário rola a página
  → Evento de scroll dispara
    → JavaScript roda getBoundingClientRect() (força layout)
      → JavaScript calcula nova posição
        → JavaScript atualiza element.style (dispara layout)
          → Navegador re-renderiza

Cada frame de scroll: Layout → JS → Layout → Paint → Composite

Pipeline de CSS Anchor Positioning

Usuário rola a página
  → Navegador atualiza posições no passe de layout normal
    → Paint → Composite

Cada frame de scroll: Layout (inclui posicionamento) → Paint → Composite

Na Prática

Pra uma página com 20 tooltips/popovers:

  • JavaScript: 20 listeners de scroll, cada um forçando recálculo de layout. Resultado: quedas de frame visíveis em scroll rápido.
  • CSS: Zero listeners. 20 posições resolvidas num único passe de layout. Zero execução JavaScript durante scroll.

Em dispositivos móveis onde cada milissegundo de main thread importa, essa diferença é o que separa scroll suave a 60fps de jank visível.


Suporte dos Navegadores e Melhoria Progressiva

Em março de 2026, CSS Anchor Positioning tem suporte completo em:

  • Chrome/Edge 125+ (desde junho de 2024)
  • Firefox 147+ (desde início de 2026)
  • Safari 26+ (desde início de 2026)

Pra navegadores antigos, use @supports:

/* Fallback */ .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); } /* Melhoria progressiva */ @supports (anchor-name: --a) { .tooltip { position: fixed; position-anchor: --trigger; position-area: top center; margin-bottom: 8px; translate: none; inset: auto; position-try-fallbacks: flip-block; } }

Conclusão

CSS Anchor Positioning é a primitiva de layout mais importante adicionada ao CSS desde Flexbox e Grid. Resolve um problema que exigiu JavaScript por mais de uma década — posicionar elementos relativos a outros elementos com tratamento de overflow.

A API é enganosamente simples. Três propriedades (anchor-name, position-anchor, position-area) cobrem 80% dos casos de uso. Os fallbacks @position-try cuidam dos outros 20%. Combinado com a Popover API, você tem tooltips, dropdowns e popovers completos sem uma linha de JavaScript.

Pra projetos novos em 2026, não tem motivo pra instalar Floating UI, Popper.js ou Tippy.js pra posicionamento ancorado básico. O navegador faz nativamente, com mais performance e menos código.

Pra projetos existentes, a migração é direta: substitua as chamadas computePosition() por propriedades CSS, remova os listeners de scroll/resize e delete a biblioteca de posicionamento das suas dependências. Seus usuários ganham scroll mais suave, seu bundle fica menor e seu código fica mais simples.

A guerra do posicionamento de tooltips acabou. CSS venceu.

CSSFrontendWeb DevelopmentPerformanceBrowser APIUI

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit