Novo Livro!
Categorias
Projetos
CSS

Trabalhando com ::highlight() usando progressive enhancement

Trabalhando com ::highlight() usando progressive enhancement

Destacar textos com CSS é útil para aplicar um destaque em um texto específico ou em fragmentos de texto durante a seleção do usuário, para enfatizar uma informação em um site ou para destacar visualmente um texto por questões de marca.

Isso é especialmente útil quando as pessoas querem escanear o documento de cima a baixo, já que a maioria não lê o documento de início: apenas escaneia.

Na web, highlights de texto são uma boa maneira de enfatizar fragmentos de texto usando o bom e velho CSS. Vão desde o ::selection, para estilizar o texto selecionado, até o ::target-text, que estiliza o texto destacado a partir de buscas do Google.

E, na opinião do autor, o pseudo-elemento mais poderoso de destaque do CSS é o ::highlight(), que aplica um destaque customizado a um fragmento de texto.

Este artigo examina como o pseudo-elemento ::highlight() funciona, a API por trás dele e um recurso de fallback para essa tecnologia, já que ela depende fortemente de JavaScript (JS).

A CSS Custom Highlight API

Normalmente, um highlight (ou texto destacado) é aquilo que aparece durante buscas na web, quando se seleciona um fragmento de texto para copiar, ou até quando se comete um erro em um editor de texto (aqueles sublinhados vermelhos ondulados).

Em CSS, é possível obter esses efeitos por meio dos pseudo-elementos de destaque, como ::search-text, ::selection, ::spelling-error e ::grammar-error. Mas e quanto a highlights simples, como na demo acima? É aí que entra a CSS Custom Highlight API.

A CSS Custom Highlight API é uma API para destacar texto em um intervalo (range) de texto usando JavaScript e CSS. Ela estende os pseudo-elementos de destaque (::search-text, ::selection, ::spelling-error) e permite customizar fragmentos de texto com ::highlight() e JavaScript. O ::highlight() é o foco deste artigo, junto com a forma de destacar textos e seus fragmentos programaticamente usando JavaScript e CSS.

Como o ::highlight() funciona

Para criar um texto destacado como no exemplo inicial, é preciso conhecer 4 passos no total, que são:

  • Criar os “baldes” de destaque, criando instâncias da classe Highlight()
  • Registrar cada instância de destaque em CSS.highlights
  • Criar objetos Range(), cada um com pontos diferentes no texto a ser destacado, e adicionar os objetos Range() às suas instâncias de destaque
  • Estilizar com o pseudo-elemento ::highlight()

Antes de partir para um exemplo, vale uma explicação, porque é aqui que a coisa pode ficar complicada. Por aqui, o costume é construir pensando primeiro em progressive enhancement. Então a primeira pergunta a se fazer, antes de qualquer coisa, é: “o que acontece quando o JS falha?”.

Suponha que, para o exemplo, a ideia seja criar dois objetos de destaque e usá-los para estilizar um texto poético simples que diz “fire and ice live inside every word”.

Como o que importa são as pessoas que acessam a página, cada texto a ser destacado recebe a tag HTML <mark>, com o atributo class definido como gold ou ice, dependendo do estilo de destaque desejado para aquele texto. Isso funciona como um estilo de destaque de fallback, caso o JavaScript falhe ou o navegador não tenha suporte a highlights customizados (a mesma lógica de uma redução progressiva de experiência).

Também é aplicado um id a cada um deles, caso o JS esteja disponível para a CSS Custom Highlight API. Com tudo isso em mente, o HTML fica assim:

<main>
<h1>Two Highlights Demo</h1>
<p id="line">
<mark id="fire" class="gold">fire</mark> and
<mark id="ice" class="ice">ice</mark> live inside
<mark id="every" class="ice">every</mark>
<mark id="word" class="gold">word</mark>
</p>
</main>

Em seguida, cada palavra marcada é consultada no JS, aplicando a propriedade firstChild a cada uma delas para obter o primeiro nó filho do elemento, que será usado mais adiante:

const fireNode = document.querySelector("#fire").firstChild;
const everyNode = document.querySelector("#every").firstChild;
const iceNode = document.querySelector("#ice").firstChild;
const wordNode = document.querySelector("#word").firstChild;

Finalmente, é possível seguir com os passos para criar um destaque customizado em CSS.

Criar instâncias da classe Highlight()

Para criar um destaque customizado, o primeiro passo é criar uma instância da classe Highlight(), que vai abrigar a estilização de destaque desejada para um texto ou fragmento de texto.

Para a demo, serão criados dois objetos de destaque usando a classe Highlight(): um para dar uma cor dourada, representando o fogo, e outro para dar uma cor azul, representando o gelo:

const goldHL = new Highlight();
const iceHL = new Highlight();

Registrar cada instância de destaque em CSS.highlights

Depois, as instâncias de destaque criadas são registradas no HighlightRegistry, por meio do método set() de CSS.highlights. Um identificador CSS válido é mapeado para a instância, para a estilização em CSS mais adiante.

CSS.highlights.set("hl-gold", goldHL);
CSS.highlights.set("hl-ice", iceHL);

Criar objetos Range() com pontos diferentes no texto a ser destacado

Neste passo, será criado um objeto Range() para cada fragmento de texto consultado anteriormente para destaque. Depois, o destaque desejado é aplicado a cada texto selecionado.

Para a primeira palavra, “fire”, cria-se um objeto Range() chamado r1, e define-se o nó inicial na primeira letra “f”, usando setStart em r1. O setStart() aceita dois valores: o nó alvo (no caso de “fire”, é fireNode) e o índice do texto no nó.

Agora, como o alvo é o texto “fire” inteiro, também é preciso definir onde o intervalo vai terminar. Isso é feito com setEnd().

O setEnd() aceita dois valores, assim como o setStart() no objeto de intervalo (r1): o nó alvo (fireNode) e o índice do texto final “e” (como o “e” de “fire”), usando fireNode.textContent.length - 1, que dá o último índice do texto.

Por fim, o objeto de intervalo é adicionado à instância definida.

const r1 = new Range();
r1.setStart(fireNode, 0);
r1.setEnd(fireNode, fireNode.textContent.length - 1);
goldHL.add(r1);

Esse passo se repete para everyNode, iceNode e wordNode. Normalmente, o ideal seria usar um loop para isso, mas, como o exemplo é bem pequeno, escrevê-lo de forma explícita dá conta do recado, especialmente por ajudar a entender como tudo funciona.

const r2 = new Range();
r2.setStart(wordNode, 0);
r2.setEnd(wordNode, wordNode.textContent.length - 1);
goldHL.add(r2);
const r3 = new Range();
r3.setStart(iceNode, 0);
r3.setEnd(iceNode, iceNode.textContent.length - 1);
iceHL.add(r3);
const r4 = new Range();
r4.setStart(everyNode, 0);
r4.setEnd(everyNode, everyNode.textContent.length - 1);
iceHL.add(r4);

Estilizar com o pseudo-elemento ::highlight()

Lembra da ideia de pensar em progressive enhancement primeiro? Pois bem: para chegar lá nesta demo em particular, é preciso estilizar antes o texto destacado com <mark> e, só depois, estilizar o pseudo-elemento ::highlight().

Para isso, o texto marcado com a classe gold foi estilizado em dourado com oklch(), com um text-shadow brilhante de cor parecida. Já o texto marcado com <mark> e a classe ice ficou azulado em oklch(), também com um text-shadow brilhante de cor parecida:

mark {
background: none;
}
mark.gold {
color: oklch(88% 0.16 75);
text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}
mark.ice {
color: oklch(82% 0.1 215);
text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}

Voilà! Ficou incrível! 🤩

Mesmo sem o ::highlight() estilizado, o resultado já funciona muito bem. Mas esse não é o único objetivo: ainda falta adicionar a estilização para os dois objetos de destaque.

::highlight(hl-gold) {
color: oklch(88% 0.16 75);
text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}
::highlight(hl-ice) {
color: oklch(82% 0.1 215);
text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}

Pronto! É praticamente a mesma estilização feita para as classes da tag <mark> e, caso o JS falhe ou o navegador não tenha suporte, os pseudo-elementos ::highlight() ficam envolvidos em um container @supports:

@supports selector(::highlight(h1-gold)) {
::highlight(hl-gold) {
color: oklch(88% 0.16 75);
text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}
::highlight(hl-ice) {
color: oklch(82% 0.1 215);
text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
}

Vale a pena usar o ::highlight()?

Este artigo explicou o que são CSS highlights, o que é a CSS Custom Highlight API e como o ::highlight() funciona no CSS com uma provisão de fallback, para quando o JavaScript não estiver disponível ou o navegador não tiver suporte. Vale lembrar que highlights visuais funcionam melhor quando acompanhados de marcação semântica — tema tratado em Acessibilidade e CSS.

Para quem quiser se aprofundar mais na CSS Custom Highlight API, a MDN tem um bom guia sobre o tema.