Tekstbereiken markeren

Met de CSS Custom Highlight API kun je willekeurige tekstbereiken op een pagina opmaken zonder de DOM-structuur te wijzigen. Hiermee kun je zoekresultaten markeren, syntaxkleuring gebruiken, cursors voor gezamenlijk bewerken gebruiken of spelling- en grammaticafouten markeren zonder tekst in extra elementen te plaatsen of innerHTML-manipulatie te gebruiken.

Kernimplementatie

Als je tekstbereiken wilt markeren, verzamel je de doeltekstnodes, maak je Range- en Highlight-objecten, registreer je ze in de HighlightRegistry en stijl je ze met het pseudoelement ::highlight().

1. Tekstnodes verzamelen en bereiken maken

Gebruik een TreeWalker om alle tekstknooppunten in het doelelement te verzamelen en maak daarna Range-objecten die verwijzen naar de tekenoffsets die je wilt markeren.

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);

Sla de lijst met tekstnodes op in het cachegeheugen en bouw deze alleen opnieuw op als de DOM-content daadwerkelijk verandert, omdat het doorlopen van de boomstructuur veel resources kost.

2. Een hoogtepunt maken op basis van de bereiken

Groepeer een of meer Range-objecten in een Highlight. Meerdere bereiken met dezelfde stijl horen in één markering.

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

3. Het hoogtepunt registreren in het register

Registreer elke Highlight onder een aangepaste naam met CSS.highlights, een Map-achtige HighlightRegistry.

// 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);

Als meerdere markeringen elkaar overlappen, gebruik je de property priority om de stapelvolgorde te bepalen. Markeringen met een hogere prioriteit worden bovenaan geplaatst.

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. Stijl met ::highlight()

Gebruik het pseudoelement ::highlight() in CSS om elke geregistreerde markering op naam op te maken.

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

Alleen een beperkte set CSS-property's werkt in ::highlight(): color, background-color, text-decoration en de volledige versies, text-shadow, -webkit-text-stroke-color, -webkit-text-fill-color en -webkit-text-stroke-width. Eigenschappen zoals background-image, font-size of padding worden genegeerd.

Toegankelijkheid

AVOID: aangepaste markeringen gebruiken als vervanging voor semantische HTML.

Aangepaste markeringen zijn puur presentatiegericht en worden niet blootgesteld aan de toegankelijkheidsstructuur. Als de gemarkeerde tekst semantisch relevant is voor het document (bijvoorbeeld een door de gebruiker geselecteerde passage), gebruik je <mark>. Reserveer aangepaste markeringen voor tijdelijke, alleen visuele effecten, zoals zoekresultaten of syntaxisaccentuering.

Markeringen mogen niet alleen afhankelijk zijn van kleur om de betekenis over te brengen. Als een markering een fout aangeeft, combineer je deze met een andere visuele indicator, zoals text-decoration: wavy underline of een aangrenzend tekstlabel. Zorg voor voldoende contrast tussen de achtergrond van de markering en de tekstkleur om te voldoen aan de WCAG 2.1-vereisten (minimaal 4,5:1 voor normale tekst).

Reservestrategieën

Voor browsers die de CSS Custom Highlight API niet ondersteunen, moet je een functionele basiservaring bieden waarbij de tekst nog steeds leesbaar is, zelfs zonder de visuele markering.

Je kunt ondersteuning detecteren voordat je de API gebruikt:

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

Als de markering essentieel is voor de gebruikerservaring, kun je terugvallen op het plaatsen van overeenkomende tekst in <mark>-elementen. Hiermee wordt de DOM gewijzigd. Zorg er dus voor dat event-listeners behouden blijven en de documentstructuur niet wordt verbroken.

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);
  }
}