Como a Nordhealth usa propriedades personalizadas em Web Components

Os benefícios de usar propriedades personalizadas em sistemas de design e bibliotecas de componentes.

David Darnes
David Darnes

Meu nome é Dave, e sou desenvolvedor sênior de front-end na Nordhealth. Trabalho no design e desenvolvimento do nosso sistema de design Nord, que inclui a criação de componentes da Web para nossa biblioteca de componentes. Eu queria compartilhar como resolvemos os problemas relacionados ao estilo dos componentes da Web usando as propriedades personalizadas de CSS e alguns dos outros benefícios do uso de propriedades personalizadas em sistemas de design e bibliotecas de componentes.

Para criar nossos componentes da Web, usamos o Lit, uma biblioteca que fornece muito código boilerplate, como estado, estilos de escopo, modelos e muito mais. O Lit é leve e também foi criado com APIs JavaScript nativas, o que significa que podemos oferecer um pacote enxuto de código que aproveita os recursos que o navegador já tem.


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);
Um componente da Web escrito com o Lit.

Mas a melhor parte dos Web Components é que eles funcionam com quase qualquer framework JavaScript ou até mesmo sem nenhum framework. Depois que o pacote principal de JavaScript é referenciado na página, usar um componente da Web é muito parecido com usar um elemento HTML nativo. O único sinal de que ele não é um elemento HTML nativo é o hífen consistente nas tags, que é um padrão para indicar ao navegador que ele é um componente da Web.

Encapsulamento de estilo do Shadow DOM

Assim como os elementos HTML nativos têm um Shadow DOM, os componentes da Web também têm. O shadow DOM é uma árvore oculta de nós dentro de um elemento. A melhor maneira de visualizar isso é abrir o Web Inspector e ativar a opção "Mostrar árvore de shadow DOM". Depois disso, tente analisar um elemento de entrada nativo no inspetor. Agora você terá a opção de abrir essa entrada e conferir todos os elementos nela. Você pode até tentar isso com um dos nossos componentes da Web. Inspecione nosso componente de entrada personalizado para conferir o shadow DOM dele.

O DOM shadow inspecionado no DevTools.
Exemplo do shadow DOM em um elemento de entrada de texto comum e no nosso componente da Web de entrada Nord.

Uma das vantagens (ou desvantagens, dependendo da sua perspectiva) do shadow DOM é o encapsulamento de estilo. Se você escrever CSS no componente da Web, esses estilos não vão vazar e afetar a página principal ou outros elementos. Eles ficam completamente contidos no componente. Além disso, o CSS escrito para a página principal ou um componente da Web pai não pode vazar para o componente da Web.

Esse encapsulamento de estilos é um benefício da nossa biblioteca de componentes. Isso nos dá mais garantia de que, quando alguém usa um dos nossos componentes, ele terá a aparência que planejamos, independentemente dos estilos aplicados à página mãe. Para garantir isso, adicionamos all: unset; à raiz, ou "host", de todos os nossos componentes da Web.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Algum código boilerplate de componente sendo aplicado à raiz sombra ou ao seletor de host.

No entanto, e se alguém que usa seu componente da Web tiver um motivo legítimo para mudar determinados estilos? Talvez haja uma linha de texto que precise de mais contraste devido ao contexto ou uma borda que precisa ser mais grossa? Se nenhum estilo puder entrar no componente, como você pode desbloquear essas opções de estilo?

É aí que entram as propriedades personalizadas de CSS.

Propriedades personalizadas do CSS

As propriedades personalizadas têm nomes muito apropriados: são propriedades CSS que você pode nomear e aplicar o valor necessário. O único requisito é que você adicione dois hifens como prefixo. Depois de declarar a propriedade personalizada, o valor pode ser usado no CSS usando a função var().


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
Exemplo do nosso framework CSS de um token de design como propriedade personalizada e usado em uma classe auxiliar.

Quando se trata de herança, todas as propriedades personalizadas são herdadas, o que segue o comportamento típico de propriedades e valores CSS normais. Qualquer propriedade personalizada aplicada a um elemento pai ou ao próprio elemento pode ser usada como um valor em outras propriedades. Usamos muito as propriedades personalizadas para nossos tokens de design, aplicando-as ao elemento raiz pelo nosso framework de CSS. Isso significa que todos os elementos na página podem usar esses valores de token, seja um componente da Web, uma classe auxiliar de CSS ou um desenvolvedor que quer extrair um valor da nossa lista de tokens.

Essa capacidade de herdar propriedades personalizadas, com o uso da função var(), é como perfuramos o DOM de sombra dos componentes da Web e permitimos que os desenvolvedores tenham um controle mais refinado ao estilizar nossos componentes.

Propriedades personalizadas em um componente da Web do Nord

Sempre que desenvolvemos um componente para nosso sistema de design, adotamos uma abordagem cuidadosa para o CSS. Gostamos de buscar um código enxuto, mas muito fácil de manter. Os tokens de design que temos são definidos como propriedades personalizadas no nosso framework CSS principal no elemento raiz.


:root {
  --n-space-m: 16px;
  --n-space-l: 24px;
  /* ... */
  --n-color-background: rgb(255, 255, 255);
  --n-color-border: rgb(216, 222, 228);
  /* ... */
}
Propriedades CSS personalizadas sendo definidas no seletor raiz.

Esses valores de token são referenciados nos nossos componentes. Em alguns casos, aplicamos o valor diretamente na propriedade CSS, mas em outros, definimos uma nova propriedade personalizada contextual e aplicamos o valor a ela.


: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);
  /* ... */
}
As propriedades personalizadas são definidas na raiz de sombra do componente e usadas nos estilos do componente. Propriedades personalizadas da lista de tokens de design também estão sendo usadas.

Também vamos abstrair alguns valores específicos do componente, mas não dos nossos tokens, e transformá-los em uma propriedade personalizada contextual. As propriedades personalizadas que são contextuais ao componente oferecem dois benefícios principais. Primeiro, isso significa que podemos usar o CSS de forma mais "seca", já que esse valor pode ser aplicado a várias propriedades dentro do 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);
}
A propriedade personalizada de preenchimento de grupo de guias é usada em vários lugares no código do componente.

Em segundo lugar, ele deixa o estado do componente e as mudanças de variação muito mais claras. É necessário mudar apenas a propriedade personalizada para atualizar todas essas propriedades quando, por exemplo, você está estilizando um estado de passagem do cursor ou ativo ou, neste caso, uma variação.


:host([padding="l"]) {
  --n-tab-group-padding: var(--n-space-l);
}
Uma variação do componente de guia em que o padding está sendo alterado usando uma única atualização de propriedade personalizada em vez de várias.

Mas o benefício mais importante é que, ao definir essas propriedades personalizadas contextuais em um componente, criamos uma espécie de API CSS personalizada para cada um dos nossos componentes, que pode ser acessada pelo usuário desse componente.


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

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Use o componente do grupo de guias na página e atualize a propriedade personalizada do padding para um tamanho maior.

O exemplo anterior mostra um dos nossos componentes da Web com uma propriedade personalizada contextual alterada por um seletor. O resultado dessa abordagem é um componente que oferece flexibilidade de estilo suficiente ao usuário, mantendo a maioria dos estilos reais em verificação. Além disso, como bônus, os desenvolvedores de componentes têm a capacidade de interceptar esses estilos aplicados pelo usuário. Se quisermos ajustar ou estender uma dessas propriedades, podemos fazer isso sem que o usuário precise mudar o código.

Essa abordagem é muito poderosa, não apenas para nós, criadores dos componentes do sistema de design, mas também para nossa equipe de desenvolvimento quando ela usa esses componentes nos nossos produtos.

Como aproveitar melhor as propriedades personalizadas

No momento em que este artigo foi escrito, não revelamos essas propriedades personalizadas contextuais na nossa documentação. No entanto, planejamos fazer isso para que nossa equipe de desenvolvimento mais ampla possa entender e aproveitar essas propriedades. Nossos componentes são empacotados no npm com um arquivo de manifesto, que contém tudo o que há para saber sobre eles. Em seguida, consumimos o arquivo de manifesto como dados quando o site de documentação é implantado, o que é feito usando o Eleventy e o recurso de dados globais. Planejamos incluir essas propriedades personalizadas contextuais neste arquivo de dados do manifesto.

Outra área que queremos melhorar é a forma como essas propriedades personalizadas contextuais herdam valores. Atualmente, por exemplo, se você quiser ajustar a cor de dois componentes de divisor, precisará segmentar ambos os componentes especificamente com seletores ou aplicar a propriedade personalizada diretamente no elemento com o atributo de estilo. Isso pode parecer bom, mas seria mais útil se o desenvolvedor pudesse definir esses estilos em um elemento de contenção ou até mesmo no nível raiz.


<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>
Duas instâncias do nosso componente divisor que precisam de dois tratamentos de cores diferentes. Um deles está aninhado em uma seção que podemos usar para um seletor mais específico, mas precisamos segmentar o divisor especificamente.

Você precisa definir o valor da propriedade personalizada diretamente no componente porque estamos definindo-o no mesmo elemento usando o seletor de host do componente. Os tokens de design globais que usamos diretamente no componente são transmitidos diretamente, sem serem afetados por esse problema, e podem até ser interceptados em elementos pais. Como podemos aproveitar o melhor dos dois mundos?

Propriedades personalizadas públicas e particulares

As propriedades personalizadas privadas foram criadas por Lea Verou. Elas são uma propriedade personalizada "privada" contextual no componente, mas definidas como uma propriedade personalizada "pública" com um substituto.



: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);
  /* ... */
}
O CSS do componente da Web do divisor com as propriedades personalizadas contextuais ajustadas para que o CSS interno dependa de uma propriedade personalizada particular, que foi definida como uma propriedade personalizada pública com um substituto.

Ao definir nossas propriedades personalizadas contextuais dessa maneira, ainda podemos fazer tudo o que fazíamos antes, como herdar valores de token globais e reutilizar valores em todo o código do componente. No entanto, o componente também herda novas definições dessa propriedade em si mesmo ou em qualquer elemento pai.


<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>
Os dois divisores novamente, mas desta vez o divisor pode ser colorido novamente adicionando a propriedade personalizada contextual do divisor ao seletor de seção. O divisor vai herdar isso, produzindo um código mais limpo e flexível.

Embora possa ser argumentado que esse método não é realmente "privado", ainda achamos que essa é uma solução elegante para um problema que nos preocupava. Quando tivermos a oportunidade, vamos resolver esse problema nos nossos componentes para que a equipe de desenvolvimento tenha mais controle sobre o uso dos componentes, aproveitando os limites que temos.

Esperamos que você tenha achado útil este insight sobre como usamos os componentes da Web com propriedades personalizadas de CSS. Conte o que você achou. Se você decidir usar algum desses métodos no seu trabalho, me encontre no Twitter @DavidDarnes. Você também pode encontrar a Nordhealth @NordhealthHQ no Twitter, assim como o restante da minha equipe, que trabalhou muito para reunir esse sistema de design e executar os recursos mencionados neste artigo: @Viljamis, @WickyNilliams e @eric_habich.

Imagem principal de Dan Cristian Pădureț