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 de front-end sênior 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 a estilização de Web Components usando propriedades personalizadas CSS e alguns dos outros benefícios do uso de propriedades personalizadas em sistemas de design e bibliotecas de componentes.

Como criamos componentes da Web

Para criar nossos Web Components, usamos a Lit, uma biblioteca que fornece diversos códigos boilerplate, como estado, estilos com escopo, modelos e muito mais. Além de ser leve, o Lit é criado com base em APIs JavaScript nativas. Isso 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 criado com Lit.

Mas o mais interessante sobre o Web Components é que eles funcionam com quase todas as estruturas de JavaScript existentes, ou até mesmo com nenhuma estrutura. Depois que o pacote JavaScript principal é referenciado na página, o uso de um componente Web é muito parecido com usar um elemento HTML nativo. O único sinal real de que ele não é um elemento HTML nativo é o hífen consistente dentro das tags, que é um padrão para indicar ao navegador que esse é um componente da Web.


// TODO: DevSite - Code sample removed as it used inline event handlers
Usando o componente Web criado acima em uma página.

Encapsulamento do estilo Shadow DOM

Assim como os elementos HTML nativos têm um Shadow DOM, o mesmo ocorre com os componentes da Web. O Shadow DOM é uma árvore escondida de nós dentro de um elemento. A melhor forma de fazer isso é abrindo seu inspetor da Web e ativando a opção "Mostrar árvore Shadow DOM". Depois de fazer isso, tente examinar um elemento de entrada nativo no inspetor. Você terá a opção de abrir essa entrada e ver todos os elementos dentro dela. Você pode até testar isso com um dos nossos componentes da Web. Inspecione nosso componente de entrada personalizado para ver o Shadow DOM.

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

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

Esse encapsulamento de estilos é um benefício da biblioteca de componentes. Isso nos dá uma garantia de que, quando alguém usar um de nossos componentes, ele terá a aparência pretendida, independentemente dos estilos aplicados à página principal. Para garantir ainda mais, 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 paralela ou ao seletor de host.

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

É aí que entram as propriedades personalizadas de CSS.

Propriedades personalizadas de CSS

As propriedades personalizadas têm nomes muito adequados. Elas são propriedades CSS que você mesmo pode nomear e aplicar o valor necessário. O único requisito é colocar dois hifens antes delas. Depois de declarar a propriedade personalizada, o valor pode ser usado no CSS por meio da função var().


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
Exemplo do nosso framework de CSS de um token de design como uma propriedade personalizada, sendo 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 regulares. Qualquer propriedade personalizada aplicada a um elemento pai ou ao próprio elemento pode ser usada como um valor em outras propriedades. Usamos bastante as propriedades personalizadas para nossos tokens de design, aplicando-as ao elemento raiz por meio do nosso framework de CSS, o que significa que todos os elementos da página podem usar esses valores de token, seja um componente da Web, uma classe auxiliar CSS ou um desenvolvedor que queira pegar um valor de nossa lista de tokens.

Essa capacidade de herdar propriedades personalizadas, com o uso da função var(), é como cruzamos o Shadow DOM dos nossos 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 estamos desenvolvendo um componente para nosso sistema de design, adotamos uma abordagem cuidadosa em relação ao CSS. Nosso objetivo é um código enxuto, mas de muita manutenção. 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 personalizadas de CSS sendo definidas no seletor raiz.

Em seguida, esses valores de token são referenciados em nossos componentes. Em alguns casos, aplicamos o valor diretamente na propriedade CSS. 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);
  /* ... */
}
Propriedades personalizadas sendo definidas na raiz paralela do componente e usadas nos estilos deles. Propriedades personalizadas da lista de tokens de design também estão sendo usadas.

Também abstrairemos alguns valores que são específicos do componente, mas não em nossos tokens, e os transformaremos em uma Propriedade personalizada contextual. As propriedades personalizadas que são contextuais ao componente nos fornecem dois benefícios principais. Primeiro, significa que o CSS pode ser mais complexo, já que esse valor pode ser aplicado a várias propriedades no 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 contextual de padding do grupo de guias está sendo usada em vários lugares no código do componente.

Segundo, o estado do componente e as mudanças de variação são muito claros. Apenas a propriedade personalizada precisa ser alterada para atualizar todas essas propriedades quando, por exemplo, você está estilizando um estado ativo, de passar o cursor ou, nesse caso, uma variação.


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

Mas o maior benefício é que, quando definimos essas propriedades personalizadas contextuais em um componente, criamos uma espécie de API CSS personalizada para cada um dos 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>
Uso do componente de grupo de guias na página e atualização da propriedade personalizada de padding para um tamanho maior.

O exemplo anterior mostra um dos nossos componentes da Web com uma propriedade personalizada contextual alterada por meio de um seletor. O resultado de toda essa abordagem é um componente que fornece flexibilidade de estilo suficiente para o usuário, mantendo a maioria dos estilos reais em controle. Além disso, como um bônus, nós, desenvolvedores de componentes, podemos interceptar os estilos aplicados pelo usuário. Se quisermos ajustar ou estender uma dessas propriedades, podemos fazer isso sem que o usuário precise alterar o código.

Consideramos essa abordagem extremamente poderosa, não apenas para nós, como criadores de nossos componentes do sistema de design, mas também para nossa equipe de desenvolvimento quando eles usam esses componentes em nossos produtos.

Como levar as propriedades personalizadas ainda mais longe

Até o momento, não revelamos essas propriedades personalizadas contextuais em 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 nosso site de documentação é implantado, o que é feito usando o Eleventy e o recurso de dados globais. Planejamos incluir essas propriedades personalizadas contextuais no arquivo de dados do manifesto.

Outra área que queremos melhorar é como essas propriedades personalizadas contextuais herdam valores. Atualmente, por exemplo, se você quisesse ajustar a cor de dois componentes divisores, precisaria segmentar esses dois 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 contêiner ou 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 componente divisor que precisam de dois tratamentos de cor diferentes. Um deles está aninhado dentro de uma seção que podemos usar para um seletor mais específico, mas precisamos segmentar o divisor de maneira específica.

Você deve definir o valor da Propriedade personalizada diretamente no componente porque nós os definimos no mesmo elemento por meio do seletor de host do componente. Os tokens de design globais que usamos diretamente no componente passam direto, sem serem afetados por esse problema e podem até ser interceptados em elementos pais. Como podemos ter o melhor dos dois mundos?

Propriedades personalizadas privadas e públicas

Propriedades personalizadas privadas são algo que foi criado por Lea Verou, que é uma propriedade personalizada "privada" contextual no próprio componente, mas definida 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 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.

Definir nossas propriedades personalizadas contextuais dessa maneira significa que 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; mas o componente também herdará de modo suave 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 divisores novamente, mas desta vez podem ser recoloridos adicionando a propriedade personalizada contextual do divisor ao seletor de seção. O divisor vai herdá-lo, produzindo um código mais limpo e flexível.

Embora possa ser argumentado que esse método não é verdadeiramente "privado", ainda achamos que é uma solução bastante elegante para um problema com o qual estávamos preocupados. Quando tivermos a oportunidade, abordaremos isso em nossos componentes para que nossa equipe de desenvolvimento tenha mais controle sobre o uso de componentes e, ao mesmo tempo, se beneficie das proteções que temos em vigor.

Esperamos que essas informações tenham sido úteis para você. Conte para nós o que você achou. Se decidir usar algum desses métodos no seu trabalho, entre em contato comigo pelo Twitter @DavidDarnes. Você também pode encontrar o Nordhealth @NordhealthHQ no Twitter e o restante da minha equipe, que se esforçaram para juntar esse sistema de design e executar os recursos mencionados neste artigo: @Viljamis, @WickyNilliams e @eric_habich.

Imagem principal por Dan Cristian P Badureē