Cómo Nordhealth usa propiedades personalizadas en componentes web

Los beneficios de usar propiedades personalizadas en sistemas de diseño y bibliotecas de componentes

David Darnes
David Darnes

Soy Dave y soy desarrollador sénior de front-end en Nordhealth. Trabajo en el diseño y desarrollo de nuestro sistema de diseño Nord, que incluye la creación de componentes web para nuestra biblioteca de componentes. Quería compartir cómo resolvimos los problemas relacionados con el diseño de componentes web con propiedades personalizadas de CSS, y algunos de los otros beneficios de usar propiedades personalizadas en sistemas de diseño y bibliotecas de componentes.

Cómo creamos componentes web

Para compilar nuestros componentes web, usamos Lit, una biblioteca que proporciona mucho código estándar, como estado, estilos con alcance, plantillas y mucho más. Lit no solo es ligero, sino que también se basa en APIs nativas de JavaScript, lo que significa que podemos entregar un paquete de código eficiente que aprovecha las funciones que ya tiene el navegador.


import {html, css, LitElement} from 'lit';

export class SimpleGreeting extends LitElement {
  static styles = css`:host { color: blue; font-family: sans-serif; }`;

  static properties = {
    name: {type: String},
  };

  constructor() {
    super();
    this.name = 'there';
  }

  render() {
    return html`

Hey ${this.name}, welcome to Web Components!

`
; } } customElements.define('simple-greeting', SimpleGreeting);
Un componente web escrito con Lit.

Sin embargo, lo más atractivo de los componentes web es que funcionan con casi cualquier framework de JavaScript existente, o incluso sin ningún framework. Una vez que se hace referencia al paquete principal de JavaScript en la página, usar un componente web es muy similar a usar un elemento HTML nativo. El único signo revelador real de que no es un elemento HTML nativo es el guion constante dentro de las etiquetas, que es un estándar para indicarle al navegador que se trata de un componente web.

Encapsulamiento de estilos de Shadow DOM

De la misma manera que los elementos HTML nativos tienen un Shadow DOM, los componentes web también lo tienen. El Shadow DOM es un árbol oculto de nodos dentro de un elemento. La mejor manera de visualizar esto es abrir el inspector web y activar la opción para "Mostrar el árbol de Shadow DOM". Una vez que lo hagas, intenta ver un elemento de entrada nativo en el inspector. Ahora tendrás la opción de abrir esa entrada y ver todos los elementos que contiene. Incluso puedes probar esto con uno de nuestros componentes web. Intenta inspeccionar nuestro componente de entrada personalizado para ver su Shadow DOM.

El DOM secundario inspeccionado en Herramientas para desarrolladores.
Ejemplo del Shadow DOM en un elemento de entrada de texto normal y en nuestro componente web de entrada de Nord.

Una de las ventajas (o desventajas, según tu perspectiva) del Shadow DOM es el encapsulamiento de estilos. Si escribes CSS dentro de tu componente web, esos estilos no pueden filtrarse y afectar la página principal ni otros elementos, ya que están completamente contenidos dentro del componente. Además, el CSS escrito para la página principal o un componente web principal no puede filtrarse en tu componente web.

Esta encapsulación de estilos es un beneficio en nuestra biblioteca de componentes. Nos brinda una mayor garantía de que, cuando alguien use uno de nuestros componentes, se verá como lo planeamos, independientemente de los estilos aplicados a la página principal. Y, para asegurarnos aún más, agregamos all: unset; a la raíz, o "host", de todos nuestros componentes web.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Se aplica código de plantilla de algunos componentes a la raíz de sombra o al selector de host.

Sin embargo, ¿qué sucede si alguien que usa tu componente web tiene un motivo legítimo para cambiar ciertos diseños? ¿Quizás hay una línea de texto que necesita más contraste debido a su contexto, o un borde necesita ser más grueso? Si no se pueden aplicar estilos a tu componente, ¿cómo puedes desbloquear esas opciones de diseño?

Ahí es donde entran en juego las propiedades personalizadas de CSS.

Propiedades personalizadas de CSS

Las propiedades personalizadas tienen un nombre muy adecuado: son propiedades de CSS a las que puedes asignarles el nombre que quieras y aplicarles el valor que necesites. El único requisito es que los prefijes con dos guiones. Una vez que hayas declarado tu propiedad personalizada, el valor se podrá usar en tu CSS con la función var().


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Ejemplo de nuestro framework de CSS de un token de diseño como una propiedad personalizada y su uso en una clase de ayuda.

En cuanto a la herencia, todas las propiedades personalizadas se heredan, lo que sigue el comportamiento típico de las propiedades y los valores de CSS normales. Cualquier propiedad personalizada que se aplique a un elemento principal o al elemento en sí se puede usar como valor en otras propiedades. Usamos mucho las propiedades personalizadas para nuestros tokens de diseño, ya que los aplicamos al elemento raíz a través de nuestro framework de CSS, lo que significa que todos los elementos de la página pueden usar estos valores de token, ya sea un componente web, una clase auxiliar de CSS o un desarrollador que quiera extraer un valor de nuestra lista de tokens.

Esta capacidad de heredar propiedades personalizadas, con el uso de la función var(), es la forma en que atravesamos el Shadow DOM de nuestros componentes web y permitimos que los desarrolladores tengan un control más preciso cuando aplican estilos a nuestros componentes.

Propiedades personalizadas en un componente web de Nord

Cada vez que desarrollamos un componente para nuestro sistema de diseño, adoptamos un enfoque reflexivo para su CSS. Nos gusta apuntar a un código eficiente pero muy fácil de mantener. Los tokens de diseño que tenemos se definen como propiedades personalizadas dentro de nuestro framework de CSS principal en el elemento raíz.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Propiedades personalizadas de CSS definidas en el selector raíz.

Luego, se hace referencia a estos valores de token dentro de nuestros componentes. En algunos casos, aplicaremos el valor directamente en la propiedad CSS, pero, en otros, definiremos una nueva propiedad personalizada contextual y le aplicaremos el valor.


:host {
  --n-tab-group-padding: 0;
  --n-tab-list-background: var(--n-color-background);
  --n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
  /* ... */
}

.n-tab-group-list {
  box-shadow: var(--n-tab-list-border);
  background-color: var(--n-tab-list-background);
  gap: var(--n-space-s);
  /* ... */
}
Propiedades personalizadas que se definen en la raíz de sombreado del componente y, luego, se usan en los estilos del componente. También se usan propiedades personalizadas de la lista de tokens de diseño.

También abstraeremos algunos valores que son específicos del componente, pero no están en nuestros tokens, y los convertiremos en una propiedad personalizada contextual. Las propiedades personalizadas que son contextuales para el componente nos brindan dos beneficios clave. Primero, significa que podemos ser más "secos" con nuestro CSS, ya que ese valor se puede aplicar a varias propiedades dentro del componente.


.n-tab-group-list::before {
  /* ... */
  padding-inline-start: var(--n-tab-group-padding);
}

.n-tab-group-list::after {
  /* ... */
  padding-inline-end: var(--n-tab-group-padding);
}
El padding del grupo de pestañas es una propiedad personalizada contextual que se usa en varios lugares dentro del código del componente.

En segundo lugar, hace que los cambios de estado y variación del componente sean muy claros: solo se debe cambiar la propiedad personalizada para actualizar todas esas propiedades cuando, por ejemplo, se diseña un estado activo o de desplazamiento, o, en este caso, una variación.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Una variación del componente de pestaña en la que el padding se cambia con una sola actualización de propiedad personalizada en lugar de varias actualizaciones.

Sin embargo, el beneficio más potente es que, cuando definimos estas propiedades personalizadas contextuales en un componente, creamos una especie de API de CSS personalizada para cada uno de nuestros componentes, a la que puede acceder el usuario de ese componente.


<nord-tab-group label="T>itl<e"
  >!<-- ... --
/nord>-t<ab-gr>oup

style
  nord-tab-group {
    --n-tab-group-padding: var(--n-space<-xl);
>  }
/style
Se usa el componente de grupo de pestañas en la página y se actualiza la propiedad personalizada de padding a un tamaño más grande.

En el ejemplo anterior, se muestra uno de nuestros componentes web con una propiedad personalizada contextual modificada a través de un selector. El resultado de todo este enfoque es un componente que proporciona suficiente flexibilidad de diseño al usuario y, al mismo tiempo, mantiene la mayoría de los diseños reales bajo control. Además, como desarrolladores de componentes, tenemos la capacidad de interceptar esos estilos aplicados por el usuario. Si queremos ajustar o extender una de esas propiedades, podemos hacerlo sin que el usuario tenga que cambiar nada de su código.

Consideramos que este enfoque es muy eficaz, no solo para nosotros como creadores de los componentes de nuestro sistema de diseño, sino también para nuestro equipo de desarrollo cuando usa estos componentes en nuestros productos.

Cómo aprovechar al máximo las propiedades personalizadas

En el momento de escribir este artículo, en realidad no revelamos estas propiedades personalizadas contextuales en nuestra documentación. Sin embargo, planeamos hacerlo para que nuestro equipo de desarrollo más amplio pueda comprender y aprovechar estas propiedades. Nuestros componentes se empaquetan en npm con un archivo de manifiesto, que contiene todo lo que se necesita saber sobre ellos. Luego, consumimos el archivo de manifiesto como datos cuando se implementa nuestro sitio de documentación, lo que se hace con Eleventy y su función de datos globales. Planeamos incluir estas propiedades personalizadas contextuales en este archivo de datos del manifiesto.

Otra área en la que deseamos mejorar es la forma en que estas propiedades personalizadas contextuales heredan valores. Actualmente, por ejemplo, si deseas ajustar el color de dos componentes de divisor, deberás segmentar ambos componentes específicamente con selectores o aplicar la propiedad personalizada directamente en el elemento con el atributo de diseño. Esto podría parecer correcto, pero sería más útil si el desarrollador pudiera definir esos estilos en un elemento contenedor o incluso en el nivel raíz.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
  }
  
  section nord-divider {
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Dos instancias de nuestro componente de divisor que necesitan dos tratamientos de color diferentes. Uno está anidado dentro de una sección que podemos utilizar para un selector más específico, pero debemos segmentar el divisor de forma específica.

El motivo por el que debes establecer el valor de la propiedad personalizada directamente en el componente es porque los definimos en el mismo elemento a través del selector del host del componente. Los tokens de diseño globales que usamos directamente en el componente pasan sin problemas, no se ven afectados por este problema y hasta se pueden interceptar en los elementos superiores. ¿Cómo podemos obtener lo mejor de ambos enfoques?

Propiedades personalizadas públicas y privadas

Las propiedades personalizadas privadas son algo que Lea Verou ha creado, que es una propiedad personalizada "privada" contextual en el componente en sí, pero establecida en una propiedad personalizada "pública" con una alternativa.



:host {
  --_n-divider-color: var(--n-divider-color, var(--n-color-border));
  --_n-divider-size: var(--n-divider-size, 1px);
}

.n-divider {
  border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
  /* ... */
}
El CSS del componente web del divisor con las propiedades personalizadas contextuales ajustadas de modo que el CSS interno dependa de una propiedad personalizada privada, que se configuró como una propiedad personalizada pública con una resguardo.

Definir nuestras propiedades personalizadas contextuales de esta manera significa que aún podemos hacer todo lo que hacíamos antes, como heredar valores de tokens globales y reutilizar valores en todo el código de nuestro componente, pero el componente también heredará correctamente nuevas definiciones de esa propiedad en sí mismo o en cualquier elemento principal.


<nord-divider></nord-divider>

<section>
  <nord-divider></nord-divider>
   <!-- ... -->
</section>

<style>
  nord-divider {
    --n-divider-color: var(--n-color-status-danger);
  }

  section {
    padding: var(--n-space-s);
    background: var(--n-color-surface-raised);
    --n-divider-color: var(--n-color-status-success);
  }
</style>
Los dos divisores nuevamente, pero esta vez el divisor se puede recolorear agregando la propiedad personalizada contextual del divisor al selector de sección. El divisor lo heredará, lo que producirá un fragmento de código más limpio y flexible.

Si bien se puede argumentar que este método no es realmente "privado", creemos que es una solución bastante elegante para un problema que nos preocupaba. Cuando tengamos la oportunidad, abordaremos este problema en nuestros componentes para que nuestro equipo de desarrollo tenga más control sobre el uso de los componentes y, al mismo tiempo, se beneficie de las medidas de protección que tenemos implementadas.

Espero que esta información sobre cómo usamos los componentes web con propiedades personalizadas de CSS te haya resultado útil. Cuéntanos qué te parece y, si decides usar alguno de estos métodos en tu trabajo, puedes encontrarme en Twitter @DavidDarnes. También puedes encontrar a Nordhealth @NordhealthHQ en Twitter, así como al resto de mi equipo, que trabajó arduamente para crear este sistema de diseño y ejecutar las funciones mencionadas en este artículo: @Viljamis, @WickyNilliams y @eric_habich.

Imagen de héroe de Dan Cristian Pădureț