Acceder a un elemento del DOM (Document Object Model) de una página web es muy sencillo y tenemos múltiples formas de conseguirlo: getElementByID(), getElementsByClassName() y por supuesto el poderosísimo querySelector() (o su gemelo querySelectorAll()). Con ellos podemos seleccionar cualquier elemento de la página desde JavaScript y luego actuar sobre él.

Sin embargo, ¿qué pasa cuando los elementos a los que necesitamos acceder son pseudo-elementos? Se trata de los elementos "ficticios" que construye el navegador cuando le añadimos al CSS ciertos atributos usando la sintaxis de los dobles dos puntos ::. Los mas conocidos son, seguro, ::before y ::after, que crean elementos ficticios en el navegador que se colocan justo después o antes del elemento al que se aplican, y que se usan para conseguir todo tipo de efectos sobre éstos. Pero también hay otros como ::first-letter (para seleccionar la primera letra del contenido del elemento), ::first-line (primera línea) o ::selection para actuar sobre el texto seleccionado por el usuario.

Nota: aunque verás muy a menudo estos elementos utilizados con tan solo dos puntos delante (#miElemento:before por ejemplo, en lugar de #miElemento::before). Esto funcionará en todos los navegadores, pero en realidad en CSS3 el prefijo utilizado deben ser los dos puntos dobles, ::. Esto es así para distinguirlos más claramente de las pseudo-clases (como :nth-child o :hover) que no son la misma cosa. La ventaja de usar solo dos puntos es que te funcionará incluso en versiones viejas de Internet Explorer, pero lo correcto es usar ::, así que tenlo en cuenta.

Como vemos son todos elementos ficticios: ninguno existe en realidad en el contenido de la página como elemento por derecho propio, sino que existen en función de cómo se definen. Es decir, por ejemplo, ::first-letter se refiere a la primera letra del contenido de un elemento que obviamente sí existe, pero no es un elemento por si mismo. Usando el pseudo-elemento ::first-letter en un selector CSS; hacemos que CSS lo considere como un elemento individual, cuando no lo es. En otros casos, como ::before o ::after son verdaderamente nuevos elementos que se crean fuera del DOM pero colocados como si estuviesen allí:

Un pseudo-elemento ::before metido dentro del DOM con sus propiedades

En la figura anterior se muestra un pseudo-elemento ::before que, como vemos está metido en el DOM (pero no pertenece a él ya que no está en el código HTML), y tiene aplicadas una serie de propiedades.

Nota: es importante fijarse en que aunque se llame "before" no va antes del elemento sobre el que se aplica sino que va antes del primer elemento hijo del elemento sobre el que se aplica. Lo mismo ocurre con ::after: va después del último elemento hijo. Mucho ojo con esto.

 

Cómo acceder a pseudo-elementos desde JavaScript

Vale, después de ese repaso general para ubicar a los lectores que no tuvieran todos los conceptos claros, vamos a plantear el tema central del post.

Podemos intentar acceder a ellos con diversos trucos, como por ejemplo usando el método querySelector(), pero no nos funcionará aunque pruebes con uno o dos dobles-puntos:

La figura muestra que querySelector() con ::before devuelve un nulo

Para lograr acceder a las propiedades de estos objetos es necesario usar una vía indirecta un tanto rebuscada. El objeto global (window) dispone de un método muy interesante llamado getComputedStyle() que nos permite obtener las propiedades CSS resultantes aplicadas a un elemento cualquiera del DOM. De este modo, una vez aplicadas todas las reglas CSS es muy fácil averiguar cuáles han quedado resultantes usando este método.

El método toma como parámetro básico la referencia a un objeto cualquiera del DOM, y devuelve un objeto de tipo CSSStyleDeclaration con todas las propiedades del elemento seleccionado, que además se pueden escribir.

Nota: además, lo que devuelve, es una colección "viva", es decir, que a medida que cambian los estilos mediante código o por la aplicación de nuevas reglas (por ejemplo, al pasarle por encima) los valores que recoge se actualizan también. O sea, no es una foto de un momento dado, sino que son valores siempre actualizados.

Por ejemplo, para obtener las propiedades resultantes del primer objeto que responde al selector .destacado > h1 basta con escribir:

var estilos = window.getComputedStyle(document.querySelector('.destacado > h1'));

Fíjate en como le paso como parámetro la referencia a un elemento de los que me interesan (querySelector() devuelve el primero, querySelectorAll() devuelve todos).

Estos estilos son accesibles mediante los nombres JavaScript de las propiedades o bien con getPropertyValue() y setPropertyvalue():

console.log('El color del elemento es: ' + estilos.getPropertyValue('color'));

Una particularidad de getComputedStyle() es que puede tomar como segundo parámetro opcional un pseudo-elemento si nos interesa acceder a él. Así que para acceder a ::before, por ejemplo, escribiríamos en el caso anterior:

var estilos = window.getComputedStyle(document.querySelector('.destacado > h1', '::before'));

y con esto estaríamos accediendo a las propiedades del pseudo-elemento en cuestión.

Es decir, la forma de acceder es a través de sus propiedades CSS usando getComputedStyle(), y no directamente.

¡Ya lo tenemos!

Un caso especial: cambiar el contenido

Lo anterior está genial si queremos acceder a cualquier propiedad CSS de un pseudo-elemento, como su color, posición, márgenes, etc... Pero existe una propiedad especial con la que tendremos problemas y que no nos servirá de mucho: el contenido.

Por ejemplo, si tengo un pseudo-elemento ::before generalmente tendré que establecerle un valor a su contenido para que se pueda visualizar. Esto se hace con la propiedad content, la cual además puede ser dinámica, usando para ello cadenas de texto, contadores, valores de atributos del elemento "padre" o una combinación de ellos.

Si intento leer el contenido de la propiedad content de un objeto obtenido de la manera que enseñé antes, con getPropertyValue(), lo que nos devolverá es el valor sin evaluar. O sea, que no podremos obtener, por ejemplo, el valor de un contador o la cadena resultante, sino sólo la expresión que se utiliza. Lo cual es muy limitante. Lo que es peor: si intentamos escribir el valor de content obtendremos un error, ya que se trata de una propiedad dinámica y no se nos permite.

Entonces ¿es imposible?

Pues no. Podemos utilizar un hack para lograrlo. Consiste en cambiarlo de manera indirecta.

Para ello voy a hacer un ejercicio sencillo: voy a construir un pequeño reloj que marcará la hora en tiempo real dentro del pseudo-elemento ::before de un elemento. Algo así:

 

como puedes ver el reloj cambia en tiempo real, lo cual se consigue manipulando el contenido de su propiedad content, algo en principio imposible. Es un ejemplo tonto pero nos ayudará a ver cómo lograr lo que buscamos.

La manera de conseguirlo es de manera indirecta: cambiando una propiedad del elemento que a su vez provoca el cambio del contenido.

El contenido de un pseudo-elemento puede conseguirse dinámicamente de varias maneras, como comentaba antes. Una de ellas es utilizando el contenido de cualquier atributo de su elemento "padre". Por ejemplo, si escribo este CSS:

#caja01:before {
    content: attr(data-content);
    position: absolute;
    top: 0.5em;
    left: 0.5em;
    padding: 0.2em;
    background: rgba(0,0,0,0.6);
    border: 1px solid black;
    color: white;
}

lo que lograré es que el contenido de ese pseudo-elemento (fíjate en a primera propiedad) se coja automáticamente desde el atributo data-content del elemento "padre". Esto además es en tiempo real: cada vez que se cambie ese atributo cambiará el contenido del pseudo-elemento.

¡Ya lo tenemos! Lo único que hay que hacer es cambiar ese atributo por JavaScript, algo extremadamente sencillo, puesto que puedo usar el método estándar de establecimiento de atributos (setAttribute()) o bien el objeto específico para manejo de atributos de datos de HTML5 (dataset).

Así, puedo escribir esto:

caja01.setAttribute('data-content', hora);

o bien esto otro, más claro todavía:

caja01.dataset.content =  hora;

Y ambos funcionarán sin problemas.

Te dejo el ejemplito del reloj aquí para descarga (ZIP, 1.9KB ).

¡Listo! De esta manera podemos modificar desde código, como necesitemos todas las propiedades de cualquier pseudo-elemento, incluso la de solo lectura content.

¡Espero que te resulte útil!

💪🏻 ¿Este post te ha ayudado?, ¿has aprendido algo nuevo?
Pues NO te pido que me invites a un café... Te pido algo más fácil y mucho mejor

Escrito por un humano, no por una IA