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

Mi nombre es Dave y soy desarrollador de frontend sénior 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. Me gustaría compartir cómo resolvimos los problemas de diseño de los componentes web con propiedades personalizadas de CSS, además de algunos de los otros beneficios de usar este tipo de propiedades 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 una gran cantidad de código estándar, como estados, 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 ofrecer 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);
Es un componente web escrito con Lit.

Pero lo más atractivo de los componentes web es que funcionan con casi cualquier framework de JavaScript existente, o incluso con 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. La única señal real de que no es un elemento HTML nativo es el guion coherente dentro de las etiquetas, que es un estándar para indicarle al navegador que se trata de un componente web.


// TODO: DevSite - Code sample removed as it used inline event handlers
Usa el componente web creado anteriormente en una página.

Encapsulamiento de estilo Shadow DOM

De la misma manera en que los elementos HTML nativos tienen un Shadow DOM, lo mismo sucede con los componentes web. 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 "Show Shadow DOM tree". Una vez que hayas hecho esto, intenta mirar 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. Prueba inspeccionar nuestro componente de entrada personalizado para ver su Shadow DOM.

Se inspeccionó el shadow DOM en Herramientas para desarrolladores.
Ejemplo de Shadow DOM en un elemento de entrada de texto normal y en nuestro componente web de entrada Nord.

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

Este encapsulamiento de estilos es un beneficio de nuestra biblioteca de componentes. Nos da más garantía de que cuando alguien use uno de nuestros componentes, se verá como esperábamos, 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;
  /* ... */
}
Cierto código estándar de componentes que se aplica a la shadow root o al selector de host

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

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

Propiedades personalizadas de CSS

Las propiedades personalizadas tienen un nombre muy apropiado: son propiedades de CSS a las que puedes asignar un nombre completo y aplicar el valor que necesites. El único requisito es que les agregues un prefijo con dos guiones. Una vez que hayas declarado tu propiedad personalizada, el valor se podrá usar en tu CSS mediante 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 que se usa en una clase auxiliar.

Cuando se trata de herencia, todas las propiedades personalizadas se heredan, de modo que se siga el comportamiento típico de las propiedades y los valores normales de CSS. Cualquier propiedad personalizada aplicada a un elemento superior, o al elemento en sí, puede usarse como un valor en otras propiedades. Hacemos un uso intensivo de las propiedades personalizadas para nuestros tokens de diseño aplicándolas en el 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 desea extraer un valor de nuestra lista de tokens.

Esta capacidad de heredar propiedades personalizadas con el uso de la función var() es la manera en que atraviesamos Shadow DOM de nuestros componentes web y permitimos que los desarrolladores tengan un control más detallado cuando definan nuestros componentes.

Propiedades personalizadas en un componente web Nord

Cuando desarrollamos un componente para nuestro sistema de diseño, adoptamos un enfoque inteligente de su CSS: nos gusta que el código sea eficiente pero que se pueda mantener. Los tokens de diseño que tenemos se definen como propiedades personalizadas dentro de nuestro framework principal de CSS 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);
  /* ... */
}
Las propiedades personalizadas de CSS que se definen en el selector raíz

Luego, se hace referencia a estos valores de token en nuestros componentes. En algunos casos, aplicaremos el valor directamente en la propiedad del 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 shadow root del componente y, luego, se usan en los diseños del componente. También se usan las propiedades personalizadas de la lista de tokens de diseño.

También abstraeremos algunos valores que son específicos del componente, pero no de nuestros tokens, y los convertiremos en una propiedad personalizada contextual. Las propiedades personalizadas que son contextuales para el componente nos proporcionan dos beneficios clave. En primer lugar, esto significa que podemos usar un CSS más "seco", 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);
}
La propiedad personalizada contextual del padding del grupo de pestañas que se usa en varios lugares dentro del código del componente

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


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

Pero el beneficio más importante 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, que el usuario de ese componente puede aprovechar.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Usar el componente de grupo de pestañas en la página y actualizar 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 mediante un selector. El resultado de todo este enfoque es un componente que proporciona suficiente flexibilidad de estilo al usuario y, al mismo tiempo, mantiene la mayoría de los estilos reales bajo control. Además, nosotros, como desarrolladores de componentes, tenemos la capacidad de interceptar los estilos que aplica 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 extremadamente 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 utilizan estos componentes en nuestros productos.

Ampliación de las propiedades personalizadas

Al momento de la redacción, no revelamos estas propiedades personalizadas contextuales en nuestra documentación. Sin embargo, planeamos 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 toda la información que debes saber sobre ellos. Luego, consumimos el archivo de manifiesto como datos cuando se implementa nuestro sitio de documentación, lo cual se hace con Eleventy y su función de datos globales. Planeamos incluir estas propiedades personalizadas contextuales en este archivo de datos de manifiesto.

Otra área en la que queremos mejorar es la forma en que estas propiedades personalizadas contextuales heredan los valores. Actualmente, por ejemplo, si deseas ajustar el color de dos componentes divisores, debes apuntar a ambos componentes específicamente con selectores o aplicar la propiedad personalizada directamente en el elemento con el atributo de estilo. Esto puede 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 divisor que necesitan dos tratamientos de color diferentes. Uno está anidado dentro de una sección que podemos usar para un selector más específico, pero tenemos que apuntar específicamente al divisor.

Para configurar el valor de propiedad personalizada directamente en el componente, debes definirlos en el mismo elemento a través del selector de host del componente. Los tokens de diseño globales que usamos directamente en el componente pasan directamente, sin que se vean afectados por este problema, e incluso se pueden interceptar en elementos superiores. ¿Cómo podemos obtener lo mejor de ambos mundos?

Propiedades personalizadas privadas y públicas

Lea Verou, que es una propiedad personalizada contextual "privada" en el componente, pero configurada como una propiedad personalizada "pública" con resguardo,



: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 divisor con las propiedades personalizadas contextuales se ajustó para que el CSS interno dependa de una propiedad personalizada privada, que se configuró como una propiedad personalizada pública con resguardo.

Definir nuestras propiedades personalizadas contextuales de este modo significa que aún podemos hacer todo lo que hacíamos antes, como heredar valores de token globales y reutilizar valores en todo el código de nuestro componente. Sin embargo, el componente también heredará correctamente definiciones nuevas 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>
De nuevo, se agregaron los dos divisores, pero esta vez puedes cambiar de color agregando la propiedad personalizada contextual del divisor al selector de secciones. El divisor lo heredará, lo que generará un fragmento de código más limpio y flexible.

Si bien se podría argumentar que este método no es en verdad "privado", todavía creemos que es una solución bastante elegante para un problema que nos preocupaba. Cuando tengamos la oportunidad, abordaremos esto 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 barreras de seguridad que implementamos.

Espero que esta información sobre cómo usamos los componentes web con las propiedades personalizadas de CSS te haya resultado útil. Danos tu opinión 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 trabajaron duro para integrar este sistema de diseño y ejecutar las funciones que se mencionan en este artículo: @Viljamis, @WickyNilliams y @eric_habich.

Hero image de Dan Cristian Pădure interfaces