Destacar intervalos de texto

Recursos da Web: Destaques personalizados

A API CSS Custom Highlight permite estilizar intervalos de texto arbitrários em uma página sem modificar a estrutura do DOM. Isso permite destacar resultados de pesquisa, colorir sintaxe, usar cursores de edição colaborativa ou marcadores de erros ortográficos e gramaticais sem precisar envolver o texto em elementos extras ou depender da manipulação de innerHTML.

Implementação principal

Para destacar intervalos de texto, é necessário coletar os nós de texto de destino, criar objetos Range e Highlight, registrá-los no HighlightRegistry e estilizá-los com o pseudoelemento ::highlight().

1. Coletar nós de texto e criar intervalos

Use um TreeWalker para coletar todos os nós de texto no elemento de destino e crie objetos Range que apontem para os deslocamentos de caracteres que você quer destacar.

const article = document.querySelector("article");

// MANDATORY: Use TreeWalker to collect text nodes — do not manipulate innerHTML.
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
  allTextNodes.push(currentNode);
  currentNode = treeWalker.nextNode();
}

// MANDATORY: Set range start/end on text nodes, not element nodes.
const range = new Range();
range.setStart(textNode, matchStartIndex);
range.setEnd(textNode, matchEndIndex);

Armazene em cache a lista de nós de texto e só a recrie quando o conteúdo do DOM realmente mudar, já que percorrer a árvore é uma operação cara.

2. Criar um destaque com base nos intervalos

Agrupar um ou mais objetos Range em um Highlight. Vários intervalos que compartilham o mesmo estilo pertencem a um único destaque.

const searchHighlight = new Highlight(...matchingRanges);

3. Registrar o destaque no registro

Registre cada Highlight com um nome personalizado usando CSS.highlights, que é um HighlightRegistry semelhante a Map.

// MANDATORY: Clear previous highlights before registering new ones
// to avoid stale ranges persisting on the page.
CSS.highlights.clear();

CSS.highlights.set("search-results", searchHighlight);

Quando vários destaques se sobrepõem, use a propriedade priority para controlar a ordem de empilhamento. Os destaques de maior prioridade são pintados por cima.

const primary = new Highlight(...primaryRanges);
primary.priority = 1;

const secondary = new Highlight(...secondaryRanges);
secondary.priority = 0; // painted first (behind primary)

CSS.highlights.set("primary", primary);
CSS.highlights.set("secondary", secondary);

4. Estilo com ::highlight()

Use o pseudoelemento ::highlight() no CSS para estilizar cada destaque registrado por nome.

::highlight(search-results) {
  background-color: #ffdd00;
  color: black;
}

Apenas um conjunto limitado de propriedades CSS funciona dentro de ::highlight(): color, background-color, text-decoration e as formas completas, text-shadow, -webkit-text-stroke-color, -webkit-text-fill-color e -webkit-text-stroke-width. Propriedades como background-image, font-size ou padding são ignoradas.

Acessibilidade

AVOID: usar destaques personalizados como substitutos do HTML semântico.

Os destaques personalizados são puramente de apresentação e não são expostos à árvore de acessibilidade. Se o texto destacado for semanticamente relevante para o documento (por exemplo, um trecho selecionado pelo usuário), use <mark>. Reserve os destaques personalizados para efeitos transitórios e apenas visuais, como resultados da pesquisa ou cores de sintaxe.

Os destaques não devem depender apenas da cor para transmitir significado. Se um destaque indicar um erro, combine-o com outro indicador visual, como text-decoration: wavy underline ou um rótulo de texto adjacente. Garanta contraste suficiente entre o fundo do destaque e a cor do texto para atender aos requisitos da WCAG 2.1 (pelo menos 4,5:1 para texto normal).

Estratégias de fallback

Para navegadores que não oferecem suporte à API CSS Custom Highlight, forneça uma experiência básica funcional em que o texto ainda seja legível, mesmo sem o destaque visual.

É possível detectar a compatibilidade antes de usar a API:

if (CSS.highlights) {
  // CSS Custom Highlight API is supported.
} else {
  // Fallback: wrap matches in <mark> elements.
}

Se o destaque for essencial para a experiência do usuário, use elementos <mark> para envolver o texto correspondente. Isso modifica o DOM, então tome cuidado para preservar os listeners de eventos e evitar quebrar a estrutura do documento.

if (!CSS.highlights) {
  // Walk text nodes and wrap matches in <mark>, preserving structure.
  const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
  const nodes = [];
  for (let n = walker.nextNode(); n; n = walker.nextNode()) nodes.push(n);

  const term = searchTerm.toLowerCase();
  for (const textNode of nodes) {
    const text = textNode.textContent;
    let pos = text.toLowerCase().indexOf(term);
    if (pos === -1) continue;

    const frag = document.createDocumentFragment();
    let last = 0;
    while (pos !== -1) {
      frag.append(text.slice(last, pos));
      const mark = document.createElement("mark");
      // textContent assignment avoids HTML injection.
      mark.textContent = text.slice(pos, pos + term.length);
      frag.append(mark);
      last = pos + term.length;
      pos = text.toLowerCase().indexOf(term, last);
    }
    frag.append(text.slice(last));
    textNode.replaceWith(frag);
  }
}