Ancore a anotação a um Range do DOM e posicione o rótulo usando os retângulos dos fragmentos renderizados pelo intervalo. Este guia apresenta uma pequena demonstração em um navegador de desktop para Mac que mantém uma nota anexada quando uma frase atravessa elementos em linha e quebra em várias linhas.
A tarefa é específica: anexar uma anotação visível a uma frase no layout comum de uma página da Web sem reconstruir o texto. Você identificará a frase, escolherá um fragmento para o rótulo e fará novas medições após alterações no layout. A MDN documenta Range.getClientRects() como a API do navegador para recuperar os retângulos ocupados por um intervalo. Range: getClientRects() method.
Por que um rótulo se afasta do texto quebrado em várias linhas?
Uma frase tem duas representações relevantes: sua posição no documento e sua geometria atual na tela. O WHATWG define um intervalo por meio de pontos de limite inicial e final, cada um composto por um nó e um deslocamento. Esses pontos podem estar em nós de texto diferentes, permitindo que um único intervalo atravesse um elemento de ênfase existente. DOM Standard.
Um único retângulo delimitador perde detalhes. Imagine uma frase que começa no meio de uma linha e termina perto do início da seguinte. O retângulo que a envolve inclui espaço vazio ao lado de ambas as linhas. Por isso, um rótulo centralizado nesse retângulo pode parecer desconectado das palavras. A Floating UI identifica explicitamente esse problema em links multilinha e seleções de intervalos. inline.
Os retângulos individuais também precisam ser interpretados. A W3C especifica que um intervalo pode retornar tanto retângulos das bordas dos elementos selecionados quanto retângulos de texto, na ordem do conteúdo. Portanto, elementos aninhados podem produzir retângulos sobrepostos; a quantidade de retângulos não é uma contagem confiável de linhas. Os limites do texto usam métricas de fonte, e não os contornos exatos dos glifos pintados. CSSOM View Module.
Qual fragmento deve receber a anotação?
Escolha uma regra de posicionamento antes de escrever o código de posicionamento. O guia de decisão a seguir separa o texto anotado do ponto em que o rótulo aparece.
| Comportamento desejado | Geometria de referência | Regra de posicionamento |
|---|---|---|
| Uma nota que acompanha uma frase | O intervalo completo | Anexe abaixo do último fragmento utilizável em um layout horizontal simples, da esquerda para a direita |
| Um rótulo próximo de uma interação com ponteiro | Os retângulos dos fragmentos do intervalo | Escolha o fragmento nas coordenadas client da interação |
| Um rótulo de visão geral centralizado | O retângulo delimitador | Aceite que o rótulo pode ficar sobre um espaço vazio |
| Uma marcação de fundo para a frase inteira | O intervalo completo | Use um destaque de texto independentemente do posicionamento do rótulo |
A demonstração usa a primeira regra. Ela fixa deliberadamente a direção e o modo de escrita para que o retângulo final seja uma referência útil para o ponto final. Considere isso uma condição do exemplo. A W3C especifica a ordem do conteúdo para esses retângulos; não garante uma ordem universal do canto superior esquerdo ao canto inferior direito. CSSOM View Module.
Para posicionamento baseado no ponteiro com a Floating UI, o middleware inline() aceita as coordenadas x e y, relativas à janela de visualização. A documentação limita essa escolha baseada em coordenadas a retângulos separados, portanto ela não deve ser descrita como um seletor de caracteres irrestrito. inline.
Crie uma demonstração completa de texto quebrado em várias linhas em um Mac
Use uma versão atual do Firefox para desktop e abra uma aba em branco em about:blank. Pressione Command + Option + K para abrir o Console da Web da página. A Mozilla documenta esse atalho e confirma que o console executa JavaScript no contexto da página. Web Console.
Execute o programa completo abaixo nesse console. Ele cria todo o conteúdo de exemplo, os estilos e os controles, portanto não é necessário nenhum projeto, importação ou biblioteca externa. A frase de exemplo atravessa um elemento de ênfase existente. A anotação é um rótulo apenas para exibição; não tem controles de edição.
(() => {
window.fragmentDemo?.destroy();
const host = document.createElement('section');
host.style.cssText = 'margin:48px 24px;padding-bottom:900px';
const p = document.createElement('p');
p.dir = 'ltr';
p.style.cssText = 'font:20px/1.6 Georgia,serif;width:24rem;max-width:80vw;writing-mode:horizontal-tb';
const a = document.createTextNode('Sample A: amber tiles follow the curved path ');
const em = document.createElement('em');
em.textContent = 'past the small blue circle and ';
const c = document.createTextNode('toward the final silver square. End.');
p.append(a, em, c);
const button = document.createElement('button');
button.textContent = 'Change text width';
const status = document.createElement('p');
host.append(button, p, status);
document.body.append(host);
const range = document.createRange();
range.setStart(a, 'Sample A: '.length);
range.setEnd(c, c.data.indexOf(' End.'));
const note = document.createElement('div');
note.textContent = 'Note A: review this phrase';
note.style.cssText = 'position:fixed;z-index:1000;background:#fff2ad;color:#222;padding:6px 10px;font:14px/1.4 system-ui;pointer-events:none';
document.body.append(note);
const style = document.createElement('style');
style.textContent = '::highlight(fragment-demo) {background-color:#dbeafe;}';
document.head.append(style);
const canHighlight = typeof Highlight !== 'undefined' && !!window.CSS?.highlights;
if (canHighlight) CSS.highlights.set('fragment-demo', new Highlight(range));
status.textContent = canHighlight ? 'The blue phrase owns Note A.' : 'Text highlighting unavailable; Note A still uses the range.';
function update() {
if (!host.isConnected) return;
const rects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0);
const r = rects.at(-1);
note.hidden = !r || range.collapsed;
if (note.hidden) return;
const maxLeft = Math.max(8, innerWidth - note.offsetWidth - 8);
note.style.left = Math.max(8, Math.min(r.left, maxLeft)) + 'px';
note.style.top = (r.bottom + 8) + 'px';
}
let narrow = false;
button.onclick = () => {
narrow = !narrow;
p.style.width = narrow ? '16rem' : '24rem';
update();
};
const observer = new ResizeObserver(update);
observer.observe(p);
window.addEventListener('resize', update);
window.addEventListener('scroll', update, true);
document.fonts.ready.then(update);
update();
window.fragmentDemo = {destroy() {
observer.disconnect();
window.removeEventListener('resize', update);
window.removeEventListener('scroll', update, true);
if (canHighlight) CSS.highlights.delete('fragment-demo');
host.remove(); note.remove(); style.remove();
}};
})();
Os pontos inicial e final excluem o prefixo Sample A: e o sufixo End.. Clique em Change text width para alternar entre duas larguras de parágrafo. O mesmo intervalo continua sendo o alvo da anotação enquanto o rótulo é reposicionado abaixo do último fragmento utilizável. Role e redimensione a janela para observar o comportamento. Remova a demonstração com window.fragmentDemo.destroy().
O programa passou por uma verificação de sintaxe JavaScript, e os deslocamentos de exemplo e os cálculos de posicionamento dos fragmentos passaram por verificações determinísticas. A renderização no navegador não foi executada aqui; as etapas acima permitem a verificação visual.
Como a demonstração preserva o fluxo e a composição do texto?
O intervalo mede o texto existente. Depois de construir o exemplo, o programa mantém os nós de texto da frase e o elemento de ênfase no lugar. Ele adiciona o rótulo separadamente e, opcionalmente, pinta a frase por meio da API CSS Custom Highlight, que a MDN documenta como funcional sem alterar a estrutura subjacente do DOM. CSS custom highlight API.
Essa distinção é importante ao adaptar a abordagem. Adicionar preenchimento, bordas ou margens em linha entre unidades de caracteres pode criar limites de composição. A W3C também identifica vertical-align que não sejam iniciais e limites de isolamento bidirecional como condições que interrompem a composição. Evite inserir invólucros de caracteres posicionados individualmente apenas para obter coordenadas; meça o resultado já produzido pelo navegador. CSS Text Module Level 3, Shaping Across Element Boundaries.
Para o texto real da aplicação, derive os pontos inicial e final do intervalo a partir dos limites de texto pretendidos. Os deslocamentos de texto do DOM usam unidades de código; eles não são contagens de glifos visíveis. Mantenha os pontos alinhados com os caracteres completos pretendidos em vez de presumir que cada caractere exibido ocupa um deslocamento. DOM Standard.
O realce é opcional. A MDN lista Highlight como Baseline 2025 e observa que navegadores mais antigos podem não oferecer suporte a ele. A verificação de recurso na demonstração preserva o posicionamento da anotação quando o realce não está disponível e exibe uma mensagem de status explicativa. Highlight.
O que precisa ser atualizado quando a página reflui?
A demonstração faz novas medições quando o parágrafo é redimensionado, a janela muda de tamanho ou há rolagem. ResizeObserver detecta alterações nas dimensões dos elementos, inclusive alterações não relacionadas ao redimensionamento da janela do navegador. Nesse caso, o callback move apenas o rótulo separado, evitando um ciclo de realimentação que alteraria repetidamente o tamanho do parágrafo medido. ResizeObserver.
O carregamento de fontes merece uma atualização própria. O programa mede imediatamente e mede de novo quando document.fonts.ready é resolvida. A MDN informa que essa promessa é resolvida após o carregamento das fontes do documento e a conclusão das operações de layout associadas. Alterações posteriores de fonte ou conteúdo iniciadas pela aplicação devem solicitar explicitamente outra medição. FontFaceSet: ready property.
Mantenha as coordenadas em um único sistema. A demonstração usa um rótulo com posicionamento fixo em uma página em branco e aplica diretamente as coordenadas client. Adicionar os deslocamentos de rolagem do documento as transformaria em coordenadas do documento. A MDN explica que os retângulos client são relativos à janela de visualização e que a adição dos deslocamentos de rolagem é a conversão usada quando se desejam coordenadas relativas ao documento. Element: getBoundingClientRect() method.
Este pequeno exemplo limita o posicionamento horizontal, mas não implementa prevenção de colisões verticais, ocultação fora da tela ou contêineres com transformações arbitrárias. Ele também não restaura uma anotação após uma recarga ou uma substituição completa do texto. Em uma aplicação, mantenha uma forma de identificar a passagem pretendida, reconstrua o intervalo após a renderização e então faça uma nova medição.
Quando você deve usar a Floating UI?
Use a Floating UI quando o rótulo precisar ser posicionado considerando os limites de recorte e de um ciclo de atualização gerenciado. A interface documentada de referência virtual oferece suporte a um intervalo por meio de dois métodos: getBoundingClientRect(), que retorna o retângulo delimitador do intervalo, e getClientRects(), que retorna os retângulos de seus fragmentos. Forneça contextElement com um elemento real que contenha o texto para que o contexto do elemento esteja disponível à detecção de recorte e atualizações. Virtual Elements.
Passe essa referência virtual para computePosition() e inclua inline() próximo ao início da matriz de middlewares, antes de flip(). Combine o posicionamento CSS do elemento flutuante com a estratégia selecionada. A documentação descreve flip() para mudar de lado quando o espaço for insuficiente e shift() para mover um rótulo dentro dos limites de recorte. inline, computePosition.
Inicie autoUpdate() apenas enquanto a anotação estiver montada e chame a função de limpeza retornada ao remover o rótulo. Os mecanismos de atualização documentados abrangem rolagem de elementos ancestrais, redimensionamento e mudanças de layout. Ainda assim, acione uma atualização após uma alteração na aplicação que mude os fragmentos do texto escolhido sem alterar as dimensões do contêiner observado. autoUpdate.
Diagnostique uma anotação que ainda parece incorreta
Siga estas verificações na ordem para que um problema de geometria não esconda um problema na identificação do texto-alvo.
- Confirme a frase. Inspecione
range.toString()e verifique os dois pontos finais antes de ajustar as coordenadas. - Inspecione os fragmentos. Registre
Array.from(range.getClientRects()). Várias entradas sobrepostas podem refletir conteúdo em linha aninhado; não interprete a quantidade como uma contagem de linhas. - Verifique a regra de posicionamento. Um retângulo delimitador, o fragmento final e um fragmento selecionado pelo ponteiro atendem a comportamentos diferentes de anotação.
- Verifique o sistema de coordenadas. Na demonstração em página em branco, mantenha o rótulo fixo em coordenadas client.
- Repita após mudanças de layout. Altere a largura, role, redimensione e aguarde o carregamento das fontes. Reconstrua o intervalo se a aplicação substituir os nós de texto.
- Verifique as premissas de layout. Para escrita vertical, direções mistas, colunas ou contêineres com transformações, defina e teste uma regra adequada para selecionar fragmentos antes de reutilizar o atalho do fragmento final da demonstração.
Uma anotação confiável começa com um intervalo de texto preciso e uma regra de posicionamento explícita. Deixe o navegador cuidar da quebra de linha e da composição, e então meça novamente os fragmentos renderizados sempre que o layout relevante mudar. Assim, a nota mantém uma relação estável com a frase, enquanto as palavras fluem naturalmente.
Fontes
- DOM Standard — WHATWG
- CSSOM View Module — W3C
- CSS Text Module Level 3 — W3C
- Range: getClientRects() method — MDN
- Element: getBoundingClientRect() method — MDN
- CSS custom highlight API — MDN
- Highlight — MDN
- ResizeObserver — MDN
- FontFaceSet: ready property — MDN
- Web Console — Mozilla
- inline — Floating UI
- Virtual Elements — Floating UI
- computePosition — Floating UI
- autoUpdate — Floating UI