CSS Highlight API

API de resaltado personalizado con CSS


La CSS Highlight API es un mecanismo de Javascript que permite resaltar fragmentos de texto de una página mediante CSS, y sin necesidad de añadir etiquetas adicionales en el HTML.

¿Qué problema resuelve?

Un caso donde es muy aplicable esta API es cuando necesitamos resaltar ciertas partes de una página, como por ejemplo, los fragmentos donde se muestra código. La forma más extendida de resaltar estos fragmentos de código es utilizando la etiqueta <pre> y <code> y librerías de resaltado de sintaxis.

Estas librerías leen y modifican el código, insertando múltiples etiquetas <span> con ciertas clases para darle estilo. La gran desventaja de esto, es que en fragmento de código muy grandes, la gran cantidad de etiquetas HTML hacen que el DOM sea muy extenso, provocando que la descarga y el renderizado sea lenta y brinde una mala experiencia.

Con CSS Highlight API podemos conseguir resaltado sin necesidad de añadir etiquetas HTML extra, y no afectando a la velocidad y carga de la página.

¿Cómo utilizar CSS Highlight API?

Para utilizar y comprender la API CSS Highlight necesitamos entender los pasos que debemos realizar. Te los explico en 4 pasos en las siguientes pestañas:

Buscar elementos

El primer paso es buscar los elementos que queremos resaltar. Para eso, hay que conocer bien las herramientas y mecanismos que nos da el DOM, como por ejemplo querySelector.

Ten en cuenta que cuando utilizas document.querySelector() lo que obtienes es un elemento HTML. Sin embargo, con esta API trabajaremos a nivel de nodos, que son unidades mucho más pequeñas.

Observa las siguientes propiedades:

PropiedadDescripción
.firstChildPrimer nodo hijo. Equivalente a .childNodes[0].
.childNodesArray (Lista de nodos) con los nodos hijos del elemento.
.lastChildÚltimo nodo hijo. Equivalente a .childNodes[n] donde n es el último nodo.

Hay dos formas de trabajar con los rangos que utilizaremos a continuación:

const element = document.querySelector("p");            // [#text]
const node = document.querySelector("p").firstChild;    // #text

En el primer caso estamos obteniendo un elemento HTML <p>, que a nivel de nodos es un array con un nodo de texto en su interior. En el segundo caso, estamos obteniendo el nodo de texto directamente, que es el primer nodo que está contenido dentro de la etiqueta HTML <p>.

Métodos de rango

Una vez tenemos los elementos donde queremos aplicar el resaltado, necesitaremos crear uno (o varios) rangos. Para ello, necesitamos un inicio de rango y un final de rango. Los métodos que podemos utilizar son los siguientes:

MétodoDescripción
.setStart(el, pos)Establece el inicio del rango en un elemento y una posición determinada.
.setStartBefore(el)Establece el inicio del rango justo antes de un elemento.
.setStartAfter(el)Establece el inicio del rango justo después de un elemento.
.setEnd(el, pos)Establece el final del rango en un elemento y una posición determinada.
.setEndBefore(el)Establece el final del rango justo antes de un elemento.
.setEndAfter(el)Establece el final del rango justo después de un elemento.

Veamos un ejemplo de uso:

<div class="container">↵
··<p>Primer texto</p>↵
··<p>Segundo texto</p></div>

<script>
const container = document.querySelector(".container");
// container.childNodes = [#text, p, #text, p]
//                        ["··", "Primer texto", "··", "Segundo texto"]

const range = new Range();
range.setStart(container, 1);
range.setEnd(container, 2);
</script>

Observa que he simbolizado los espacios con · y los ENTER con un , por lo que los childNodes no son dos etiquetas <p> como podríamos pensar en un principio, sino que son:

  • Un nodo de texto con una nueva línea y dos espacios
  • El contenido de texto de la primera etiqueta <p>
  • Otro nodo de texto con una nueva línea y dos espacios
  • El contenido de texto de la segunda etiqueta </p>
  • Otro nodo de texto con una nueva línea

A los métodos .setStart() y .setEnd() si le pasamos una etiqueta HTML, los números harán referencia a la posición del nodo. Un 1 hace referencia al inicio del primer <p> y un 2 hace referencia al inicio del fragmento de texto con espacios, es decir, al final del primer <p> (son el mismo).

Si trabajamos a nivel de nodos, trabajaríamos a nivel de carácteres:

const nodes = document.querySelector(".container").childNodes;

const range = new Range();
range.setStart(nodes[1], 0);
range.setEnd(nodes[1], 6);

En este caso, con nodes[1] obtenemos el nodo de texto del primer <p> y con 0 y 6 obtenemos las primeras 6 letras del nodo de texto, es decir sólo Primer. Más adelante veremos unos ejemplos de estos casos.

Por último, mediante los métodos .setStartBefore() y .setStartAfter() simplemente tenemos un atajo para indicar un elemento y obtener el inicio o final de dicho elemento, estableciéndolo como inicio del rango, o lo mismo respecto al final del rango con los que comienzan por .setEnd.

CSS Highlights

Una vez tenemos los rangos, podemos crear nuestros highlight con un nombre y añadirlos al registro de Highlights para que lo reconozca nuestra página.

Antes de nada, observa los métodos principales de nuestro registro CSS.highlights. Funciona de forma muy similar a un Map de Javascript:

MétodoDescripción
.has(name)Comprueba si existe un «highlight» con ese nombre en el registro.
.get(name)Devuelve el «highlight» con el nombre indicado en el registro.
.set(name, value)Añade un «highlight» con el nombre indicado al registro.
.sizeNúmero de «highlights» que tiene el registro de la API.
.delete(name)Elimina el «highlight» con el nombre indicado del registro.
.clear()Elimina todos los «highlights» del registro.

Observa como lo aplicaríamos:

const highlight = new Highlight(range);   // Creamos un highlight con uno o varios rangos
CSS.highlights.set("rango", highlight);   // Lo añadimos al registro con el nombre "rango"

Resaltando con CSS Highlight

Finalmente, como tenemos los highlights registrados con un nombre en el navegador, podemos hacer uso de CSS y el nuevo pseudoelemento ::highlight() para darle estilo, indicando el nombre del highlight:

::highlight(rango) {
  background: indigo;
  color: white;
}

Ejemplos de CSS Highlight

Ahora que ya sabemos como funciona, veamos algunos ejemplos en acción. Vamos a crear un rango simple utilizando el nodo de un elemento HTML directamente, y luego vamos a crear varios rangos utilizando nodos específicos.

Seleccionando elementos

En este primer ejemplo, vamos a partir de un elemento <div> que tiene 3 párrafos. Recuerda que antes de cada <p> hay un nodo con espacios en blanco:

<div class="container">
  <p class="first">Este es un primer párrafo de texto.</p>
  <p class="second">Este es un segundo párrafo de texto.</p>
  <p class="third">Este es un tercer párrafo de texto.</p>
</div>

<style>
::highlight(paragraph) {
  background: indigo;
  color: white;
  padding: 0.25rem;
}
</style>
const container = document.querySelector(".container");
// NodeList(7) [text, p.first, text, p.second, text, p.third, text]

const range = new Range();
range.setStart(container, 3);
range.setEnd(container, 4);

const highlight = new Highlight(range);

CSS.highlights.set("paragraph", highlight);

Si te fijas en la parte de Javascript, el container tiene 7 nodos. En nuestro ejemplo, como indicamos container indicamos que use el nodo número 3 como inicio y el nodo número 4 como final, o lo que es lo mismo, un rango desde el principio del texto del segundo <p> hasta el inicio del nodo de texto con nueva línea y espacios (excluyendo el contenido de texto).

Seleccionando texto

En este segundo ejemplo, tenemos un <div> que contiene un único <p> que dentro tiene varios nodos. Concretamente, un nodo de texto con espacios, el texto del <strong>, otro nodo de texto con párrafo de, el texto del <span> y un nodo de texto con el . del final:

<div class="container">
  <p>Este es un <strong>primer</strong> párrafo de <span>texto</span>.</p>
</div>

<style>
::highlight(paragraph) {
  background: indigo;
  color: white;
  padding: 0.25rem;
}
</style>
const textNodes = document.querySelector(".container p").childNodes;
// textNodes = NodeList(5) [text, strong, text, span, text]
//                         ["Este es un ", "primer", " párrafo de ", "texto", "."]

const range1 = new Range();
range1.setStart(textNodes[0], 5);
range1.setEnd(textNodes[0], 7);

const range2 = new Range();
range2.setStart(textNodes[2], 1);
range2.setEnd(textNodes[2], 8);

const highlight = new Highlight(range1, range2);

CSS.highlights.set("paragraph", highlight);

En este caso creamos dos rangos a nivel de texto. El primer rango va desde 5 a 7 del primer nodo, por lo que resaltamos el texto es. El segundo rango va desde 1 a 8 del tercer nodo, por lo que resaltamos el texto párrafo.

Como ves, se pueden crear rangos múltiples con varios fragmentos.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev