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 gostaria de compartilhar como resolvemos os problemas relacionados ao estilo de componentes da Web usando as propriedades personalizadas de CSS, além de alguns 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 a Lit, uma biblioteca que fornece muito código boilerplate, como estado, estilos com escopo, modelos e muito mais. Além de ser leve, o Lit foi desenvolvido com APIs JavaScript nativas. Isso significa que podemos oferecer um conjunto enxuto de código que aproveita os recursos do 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);
Um componente da Web escrito com o Lit.

Mas o mais interessante sobre os Web Components é que eles funcionam com quase qualquer estrutura JavaScript existente, ou até mesmo com nenhuma estrutura. Depois que o pacote JavaScript principal é referenciado na página, o uso de um componente Web é muito semelhante ao uso de um elemento HTML nativo. O único indício real de que ele 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.


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

Encapsulamento do estilo 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 do Shadow DOM". Depois de fazer isso, tente examinar um elemento de entrada nativo no inspetor. Agora você terá a opção de abrir essa entrada e ver todos os elementos dentro dela. Você pode até mesmo testar isso com um dos nossos componentes da Web. Confira nosso componente de entrada personalizado para ver o Shadow DOM.

O DOM sombra inspecionado no DevTools.
Exemplo do Shadow DOM em um elemento de entrada de texto comum e no nosso Web Component 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 Web principal não pode vazar para o seu componente Web.

Esse encapsulamento de estilos é um benefício em 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 ainda mais, adicionamos all: unset; à raiz, ou "host", de todos os 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 mudar 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 no seu componente, como você pode desbloquear essas opções de estilo?

É aí que entram as propriedades personalizadas do CSS.

Propriedades personalizadas de CSS

As propriedades personalizadas têm nomes muito apropriados: são propriedades CSS que você pode nomear e aplicar o valor necessário. A única exigência é que eles sejam prefixados com dois hifens. 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 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 regulares. Qualquer propriedade personalizada aplicada a um elemento pai ou ao próprio elemento pode ser usada como um valor em outras propriedades. Fazemos um grande uso das propriedades personalizadas para nossos tokens de design ao aplicá-las ao elemento raiz por meio de nosso CSS Framework. Isso significa que todos os elementos da página podem usar esses valores de token, sejam eles um componente da Web, uma classe auxiliar de CSS ou um desenvolvedor que queira extrair um valor de 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 Web do Nord

Sempre que desenvolvemos um componente para nosso sistema de design, adotamos uma abordagem cuidadosa ao CSS. Gostamos de buscar um código enxuto, mas que seja fácil de manter. Os tokens de design que temos são definidos como propriedades personalizadas em 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 definidas no seletor raiz.

Esses valores de token são referenciados nos nossos componentes. Em alguns casos, aplicamos o valor diretamente na propriedade CSS. Para outros, na verdade, 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 são definidas na raiz paralela do componente e usadas nos estilos dele. As 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>
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 um seletor. O resultado dessa abordagem é um componente que oferece flexibilidade suficiente de estilo ao usuário, mantendo a maioria dos estilos reais sob controle. Além disso, como 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 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 levar as propriedades personalizadas ainda mais longe

No momento, 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 nesse 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 os 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 componente divisor que precisam de dois tratamentos de cor 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 esse valor 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 particulares foram criadas por Lea Verou, que é uma propriedade personalizada contextual "privada" 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 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ções. O divisor vai herdar esse elemento, produzindo um código mais limpo e flexível.

Embora possa-se argumentar que esse método não é verdadeiramente "particular", ainda acreditamos que essa é uma solução elegante para um problema que nos preocupamos. Quando tivermos a oportunidade, vamos abordar isso em nossos componentes para que nossa equipe de desenvolvimento tenha mais controle sobre o uso dos componentes e, ao mesmo tempo, aproveite as proteções que estabelecemos.

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 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, bem como o restante da minha equipe, que se esforçaram 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ț