CSS en LitElement

Ya hablamos de CSS en WebComponents, donde tratamos formas de manejar estilos en componentes nativos, utilizando estrategias conocidas como estilos en línea, hojas de estilo externas o variables css, junto a nuevos estándares como CSS Scoping o CSS Parts.

LitElement: CSS estático, CSS dinámico, classMap y styleMap

Muchas de estas formas, al ser nativas, se pueden aplicar en LitElement, pero en este apartado nos interesará más conocer la estrategia habitual que se suele seguir en LitElement para gestionar y mantener estilos CSS, y es eso en lo que vamos a centrarnos.

Estilos CSS estáticos

Por norma general, en LitElement nos vamos a encontrar una forma de definir estilos CSS de forma estática, es decir, se van a aplicar a todas las instancias del mismo componente. Más adelante veremos un caso diferente, en el que podríamos querer tener diferentes estilos para una instancia concreta del componente.

En nuestro primer ejemplo, cada elemento <app-element> tendrá estos estilos CSS estáticos, ya que el navegador se encargará de recogerlos una sola vez y aplicarlo a todos los componentes del mismo tipo, siendo así bastante eficiente. Esta es la forma más habitual de definirlos, y se implementan mediante un getter estático styles(), haciendo uso de la función ayudante css:

<app-element></app-element>

<script type="module">
  import { LitElement, html, css } from 'https://unpkg.com/[email protected]?module';

  customElements.define("app-element", class extends LitElement {
    static get styles() {
      return css`
        div {
          background: hotpink;
          color: white;
        }
      `;
    }

    render() {
      return html`<div>Un elemento con estilo.</div>`;
    }
  });
</script>

Al igual que el método render() espera que devuelvas una plantilla HTML generada con la función html de lit-html, el getter styles() espera que devuelvas una plantilla de CSS generada con css.

Sin embargo, si por alguna razón necesitaramos organizarnos creando varias plantillas de CSS, para finalmente mezclarlas en una, podríamos devolver un que contenga varias plantillas de CSS. LitElement las fusionará correctamente, aplicando lo que se suele denominar composición:

static get styles() {
  return [
    css`div { background: hotpink }`,
    css`div { color: white }`
  ];
}

También podríamos heredar los estilos estáticos de un componente padre con super.styles(), si extendieramos de otro componente padre que tiene un getter de styles() estáticos, mezclándolos con los estilos del componente hijo.

Incluso, podríamos crear constantes fuera de la clase del componente con plantillas CSS, o importarlas utilizando ESM desde otro fichero:

// Exportando plantilla de CSS (en otro fichero diferente)
export const buttonStyles = css`button { /* ... */ }`;

// Importando plantillas CSS para utilizarlas
import { buttonStyles, purpleTheme } from "./AppElement.js";

Posteriormente, podríamos importarlas y utilizarlas dentro de nuestro componente, o mezclarlas con estilos ya existentes a través del mencionado , que nos permite utilizar composición sobre estilos CSS.

Estilos CSS dinámicos

Hasta ahora hemos visto «estilos CSS estáticos», la forma preferida de añadir CSS en componentes LitElement, ya que se aplican a todas las instancias de ese mismo componente, y los estilos son evaluados una única vez, siendo reutilizados en todos los componentes del mismo tipo. Esto hace que sea muy eficiente su procesamiento.

Sin embargo, nos podría interesar añadir estilos específicos que dependan de la lógica Javascript de una instancia particular, porque por ejemplo, dependen de variables de clase. Estaríamos hablando de lo que se denomina estilos CSS dinámicos.

Para hacer estos cambios dinámicos, que varían en cada instancia, se recomienda:

  • Utilizar CSS Custom Properties (preferida), ya que incluso puede atravesar Shadow DOM.
  • Utilizar una etiqueta <style> en la plantilla HTML, con los cambios CSS específicos.
  • Utilizar un fichero CSS externo, mediante la etiqueta <link>.

Veamos un ejemplo de lo que, probablemente, sería lo más intuitivo cuando queremos colocar lógica dinámica en un componente. Ten en cuenta que en este ejemplo tenemos dos instancias de componente, una normal y otra con una clase rainbow:

<app-element></app-element>
<app-element class="rainbow"></app-element>

<script type="module">
  import { LitElement, html, css } from 'https://unpkg.com/[email protected]?module';

  customElements.define("app-element", class extends LitElement {
    static get styles() {
      return css`
        div {
          color: white;
        }
      `;
    }

    render() {

      this.bgcolor = this.classList.contains("rainbow")
        ? "linear-gradient(to right, red, yellow, green, blue, purple)"
        : "grey";

      return html`
        <style>
          div {
            background: ${this.bgcolor};
          }
        </style>
        <div>Un elemento con estilo.</div>
      `;
    }
  });
</script>

En el render(), antes de devolver la plantilla, realizamos una condición con un operador ternario que comprueba si el componente tiene la clase rainbow, y de ser así, modifica la variable de clase que contiene el color de fondo. Esta variable se aplica en un elemento <style> en el marcado HTML, ya que al ser dinámico no se puede aplicar en el CSS estático por componente.

Sin embargo, esta forma de trabajar puede acarrear problemas de performance o incluso de seguridad (como veremos más adelante), por lo que ideal sería siempre intentar realizarlo de una forma estática si es posible.

En el siguiente ejemplo hacemos lo mismo que en el anterior, pero de una forma estática:

<app-element></app-element>
<app-element class="rainbow"></app-element>

<script type="module">
  import { LitElement, html, css } from 'https://unpkg.com/[email protected]?module';

  customElements.define("app-element", class extends LitElement {
    static get styles() {
      return css`
        :host(.rainbow) {
          --app-bgcolor: linear-gradient(to right, red, yellow, green, blue, purple);
        }
        div {
          background: var(--app-bgcolor, grey);
          color: white;
        }
      `;
    }

    render() {
      return html`<div>Un elemento con estilo.</div>`;
    }
  });
</script>

En este caso, estamos usando CSS Scoping para aplicar estilos al componente dependiendo de la clase que tenga. Utilizamos variables CSS para propagar los estilos en el componente (podríamos hacerlo incluso desde fuera del componente).

Recuerda que incluso podrías utilizar Javascript para modificar variables CSS si lo necesitas, utilizando, por ejemplo, el método .style.setProperty().

Problemas en estilos dinámicos

El problema principal que tenemos al declarar estilos dinámicos en nuestros componentes, es que perdemos la eficiencia de los estilos estáticos, que se evaluan al crear el componente y no se tienen que volver a evaluar al crear nuevas instancias de componentes.

Sin embargo, muchas veces no es posible utilizar estilos estáticos, por lo que se suele optar por inyectar directamente propiedades Javascript en el código, lo que se considera en LitElement un antipatrón (mala práctica que se debe evitar):

  render() {
    return html`
    <style>
      .element { background: ${this.bgcolor}; }
    </style>
    <div class="element">Element</div>`;
  }

Lo recomendable es seguir una serie de pautas:

En primer lugar, crea las plantillas HTML prerenderizadas con la función de ayuda html. Intenta que dichas plantillas contengan la etiqueta <style> con los estilos a aplicar en cada caso:

const rainbowStyles = html`
<style>
  .element { background: linear-gradient(to right, red, yellow, green, blue, purple); }
</style>`;

const defaultStyles = html`
<style>
  .element { background: grey; }
</style>`;

Luego, puedes utilizar una variable de clase para indicar los estilos específicos que va a utilizar la instancia del componente, añadiéndola por separado de los estilos que utilizarás en el componente:

  constructor() {
    super();
    this.specificss = this.classList.contains("rainbow") ? rainbowStyles : defaultStyles;
  }

  render() {
    return html`
    <style>
      .element {
        color: white;
      }
    </style>
    ${this.specificss}
    <div class="element">Element</div>`;
  }

Esto permitirá a LitElement (si su propiedad es reactiva y cambia), no vuelva a renderizar todo el etiquetado HTML, realizando mucho trabajo extra, sino que sólo actualice lo que ha cambiado y es necesario. No obstante, recuerda que siempre es preferible encontrar un enfoque estático para los estilos y/o utilizar css custom properties.

Ayudante classMap

Una forma que quizás nos puede resultar más interesante a los que estamos acostumbrados a trabajar con clases CSS, es la que nos proporciona el ayudante classMap de lit-html, la librería hermana menor de LitElement.

Este ayudante, nos permite gestionar los estilos de un elemento por medio de las clases que tiene en el HTML, permitiéndonos gestionarlo también por medio de un objeto Javascript y sus valores. Para utilizarlo, hagamos lo siguiente:

  • Importamos el ayudante classMap de la librería lit-html.
  • Definimos en styles() las clases CSS con sus estilos correspondientes.
  • Creamos un objeto en el constructor(), donde cada clave es el nombre de la clase.
  • Si una de esas claves está a true, la clase se incluirá. Si está a false no lo hará.

<app-element></app-element>
<app-element class="rainbow"></app-element>

<script type="module">
  import { LitElement, html, css } from "https://unpkg.com/[email protected]?module";
  import { classMap } from "https://unpkg.com/[email protected]/directives/class-map?module"

  customElements.define("app-element", class extends LitElement {
    constructor() {
      super();
      this.classes = {
        rainbow: this.classList.contains("rainbow"),
        hollow: false,
        hover: false
      }
    }

    static get styles() {
      return css`
        .rainbow {
          background: linear-gradient(to right, red, yellow, green, blue, purple);
        }
        .hollow {
          background: transparent;
          color: purple;
        }
        .hover {
          background: orangered;
          color: white;
        }
        div {
          background: grey;
          color: white;
          padding: 5px;
        }
      `;
    }

    render() {
      return html`<div class=${classMap(this.classes)}>
        Un elemento con estilo.
      </div>`;
    }
  });
</script>

Por último, no te olvides de incluir la referencia al ayudante classMap() en la clase del elemento que necesitas. Por parámetro le indicaremos el objeto que contiene las claves con las clases a añadir. Todo esto hará muy sencillo gestionar this.classes para añadir o eliminar las clases CSS que queramos aplicar.

Ten en cuenta que classMap no hace los cambios de forma reactiva. Si queremos renderizar los cambios, debemos hacer un render manual con this.render().

Ayudante styleMap

De la misma forma que tenemos el ayudante classMap para gestionar clases CSS de un elemento con un objeto Javascript, tenemos el ayudante styleMap, que no es más que el equivalente para gestionar los estilos en línea de un elemento.

En este caso, hacemos lo siguiente:

  • Importamos el ayudante styleMap de la librería lit-html.
  • Creamos en el constructor() un objeto this.styles, donde cada clave es una propiedad CSS.
  • Los valores de cada clave, serán los valores aplicados al elemento.

<app-element></app-element>
<app-element class="rainbow"></app-element>

<script type="module">
  import { LitElement, html, css } from "https://unpkg.com/[email protected]?module";
  import { styleMap } from "https://unpkg.com/[email protected]/directives/style-map?module"

  customElements.define("app-element", class extends LitElement {
    constructor() {
      super();
      this.styles = {
        background: this.classList.contains("rainbow")
          ? "linear-gradient(to right, red, yellow, green, blue, purple)"
          : "grey",
        margin: "5px",
        border: "4px solid black",
        fontFamily: "Scope One"
      }
    }

    static get styles() {
      return css`
        div {
          background: grey;
          color: white;
          padding: 5px;
        }
      `;
    }

    render() {
      return html`<div style=${styleMap(this.styles)}>
        Un elemento con estilo.
      </div>`;
    }
  });
</script>

Por último, y al igual que en el caso anterior, hay que aplicar al atributo style el ayudante styleMap(), pasándole por parámetro el nombre del objeto con los estilos, en este caso, this.styles.

Ten en cuenta que, la nomenclatura en CSS suele ser kebab-case (margin-top, font-family, flex-direction...), pero en Javascript esta nomenclatura no está permitida, por lo que sus equivalentes son en pascalCase (marginTop, fontFamily, flexDirection...).

Ayudante unsafeCSS

En algunos casos, puede que hayas intentado interpolar una variable o constante en una plantilla de CSS directamente, algo parecido a lo siguiente, siendo color una constante fuera de la clase:

static get styles() {
  return css`
    div {
      background: ${color};
      color: white;
      padding: 5px;
    }`;
}

Al hacerlo es posible que obtengas el siguiente error:

Uncaught Error: Value passed to 'css' function must be a 'css' function result: undefined. Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.

En LitElement se intenta promover esta forma de trabajar, puesto que si las variables o constantes tienen información que proviene del usuario, podrían tener código malicioso o ser vectores de problemas de privacidad o seguridad. Por ejemplo, un usuario malintencionado podría insertar un código CSS que cargue una imagen de un servidor de terceros.

Teniendo esto en cuenta, es posible utilizar la función unsafeCSS, importándola de LitElement y aplicándola a las variables o constantes en cuestión:

import { css, unsafeCSS } from "https://unpkg.com/[email protected]?module";

/* ... */

static get styles() {
  return css`
    div {
      background: ${unsafeCSS(color)};
      color: white;
      padding: 5px;
    }`;
}

Eso sí, recuerda siempre que esto podría ser un posible foco de problemas de seguridad, y habría que asegurar siempre que el código CSS a insertar en la función unsafeCSS es seguro y no proviene del usuario.

Manz
Publicado por Manz

Docente, divulgador informático y freelance. Autor de Emezeta.com, es profesor en la Universidad de La Laguna y dirige el curso de Programación web FullStack y Diseño web FrontEnd de EOI en Tenerife (Canarias). En sus ratos libres, busca GIF de gatos en Internet.