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. Quero compartilhar como resolvemos os problemas de estilização de componentes da Web usando propriedades personalizadas de CSS e alguns dos outros benefícios de usar propriedades personalizadas em sistemas de design e bibliotecas de componentes.

Como criamos componentes da Web

Para criar nossos componentes da Web, usamos o Lit (em inglês), uma biblioteca que fornece muito código clichê, 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 de código enxuto 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 Lit.

Mas o mais interessante sobre os Web Components é que eles funcionam com quase qualquer framework JavaScript atual ou até mesmo sem nenhum framework. Depois que o pacote principal do JavaScript é referenciado na página, usar um Web Component é muito parecido com usar um elemento HTML nativo. O único sinal real de que não é um elemento HTML nativo é o hífen consistente nas tags, que é um padrão para indicar ao navegador que se trata de 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 inspetor da Web e ativar a opção "Mostrar árvore do shadow DOM". Depois de fazer isso, 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é 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 comum e no componente da Web de entrada do Nord.

Uma das vantagens (ou desvantagens, dependendo da sua perspectiva) do Shadow DOM é o encapsulamento de estilo. Se você escrever CSS no seu componente da Web, esses estilos não poderã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.

Essa encapsulação de estilos é um benefício na nossa biblioteca de componentes. Isso garante que, quando alguém usa um dos nossos componentes, ele vai aparecer como pretendemos, independente 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 clichê de componente sendo aplicado à raiz shadow 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 uma linha de texto precise de mais contraste devido ao contexto ou uma borda precise ser mais grossa. Se nenhum estilo puder entrar no seu componente, como você pode desbloquear essas opções de estilização?

É aí que entram as propriedades personalizadas de CSS.

Propriedades personalizadas do CSS

As propriedades personalizadas têm um nome muito adequado: são propriedades CSS que você pode nomear e aplicar o valor necessário. O único requisito é que eles sejam prefixados com dois hífens. Depois de declarar a propriedade personalizada, o valor pode ser usado no CSS com a função var().


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

.n-color-accent-text {
  color: var(--n-color-accent);
}
Exemplo da nossa estrutura CSS de um token de design como uma propriedade personalizada e 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 muito as propriedades personalizadas para nossos tokens de design, aplicando-as ao elemento raiz pelo nosso framework 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 queira extrair um valor da nossa lista de tokens.

Essa capacidade de herdar propriedades personalizadas, com o uso da função var(), é como atravessamos 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 Nord

Sempre que desenvolvemos um componente para nosso sistema de design, adotamos uma abordagem cuidadosa para o CSS. Nosso objetivo é ter um código simples, mas muito fácil de manter. Os tokens de design que temos são definidos como propriedades personalizadas no nosso principal framework CSS 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 definidas no seletor raiz.

Esses valores de token são referenciados nos nossos componentes. Em alguns casos, vamos aplicar o valor diretamente na propriedade CSS, mas, em outros, vamos definir uma nova propriedade personalizada contextual e aplicar 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 definidas na raiz shadow do componente e usadas nos estilos dele. 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 contextuais ao componente oferecem dois benefícios principais. Primeiro, isso significa que podemos ser mais "secos" com nosso CSS, 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);
}
O padding do grupo de guias, uma propriedade personalizada contextual, sendo usado em vários lugares no código do componente.

Em segundo lugar, ele torna as mudanças de estado e variação do componente muito limpas. Apenas a propriedade personalizada precisa ser alterada para atualizar todas essas propriedades quando, por exemplo, você está estilizando um estado de passar o 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 é alterado usando uma única atualização de propriedade personalizada em vez de várias atualizações.

Mas o benefício mais importante é que, quando definimos essas propriedades personalizadas contextuais em um componente, criamos uma espécie de API CSS personalizada para cada um deles, que pode ser usada pelo usuário.


<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
Usando o componente de grupo de guias na página e atualizando a 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 um seletor. O resultado de toda essa abordagem é um componente que oferece flexibilidade de estilo suficiente para o usuário, mantendo a maioria dos estilos reais sob controle. Além disso, como desenvolvedores de componentes, temos 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 útil, não apenas para nós como criadores dos componentes do sistema de design, mas também para nossa equipe de desenvolvimento quando ela usa esses componentes nos nossos produtos.

Como usar propriedades personalizadas

No momento da redação, não revelamos essas propriedades personalizadas contextuais na 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 é preciso 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 dele. Planejamos incluir essas propriedades personalizadas contextuais no arquivo de dados do manifesto.

Outra área que queremos melhorar é como essas propriedades personalizadas contextuais herdam valores. Por exemplo, se você quisesse ajustar a cor de dois componentes divisores, precisaria segmentar os 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 contido 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 está aninhado em uma seção que podemos usar para um seletor mais específico, mas precisamos segmentar o divisor especificamente.

O motivo de você precisar definir o valor da propriedade personalizada diretamente no componente é que estamos definindo esses valores no mesmo elemento usando o seletor de host do componente. Os tokens de design globais que usamos diretamente no componente passam sem serem afetados por esse problema e podem até ser interceptados em elementos pai. 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 privada, que foi definida como uma propriedade personalizada pública com um substituto.

Definir nossas propriedades personalizadas contextuais dessa forma 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. No entanto, o componente também vai herdar normalmente novas definições dessa propriedade em si 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 é possível mudar a cor deles adicionando a propriedade personalizada contextual ao seletor de seção. O divisor vai herdar isso, produzindo um código mais limpo e flexível.

Embora se possa argumentar que esse método não é realmente "privado", ainda achamos que é uma solução bastante elegante para um problema que nos preocupava. Quando tivermos a oportunidade, vamos resolver isso nos nossos componentes para que a equipe de desenvolvimento tenha mais controle sobre o uso deles, sem deixar de aproveitar os limites que temos em vigor.

Espero que você tenha achado útil esta visão geral de como usamos componentes da Web com propriedades personalizadas de CSS. Dê sua opinião e, se decidir usar algum desses métodos no seu trabalho, me encontre no Twitter @DavidDarnes. Você também pode encontrar a Nordhealth @NordhealthHQ no Twitter, além do 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ț