Os benefícios de usar propriedades personalizadas em sistemas de design e bibliotecas de componentes.
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, uma biblioteca que fornece muitos 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, o que significa que podemos fornecer 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);
Mas o mais interessante sobre os componentes da Web é que eles funcionam com quase todos os frameworks de JavaScript atuais, 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 DOM paralelo
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 é abrindo o inspetor da Web e ativando a opção "Mostrar árvore do shadow DOM". Depois disso, tente analisar 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é testar isso com um dos nossos componentes da Web: inspecione nosso componente de entrada personalizado para ver o Shadow DOM dele.
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.
Esse encapsulamento de estilos é um benefício da nossa biblioteca de componentes. Isso nos dá mais garantia de que, quando alguém usar um dos nossos componentes, ele vai ter a aparência que pretendemos, independentemente dos estilos aplicados à página mãe. Para garantir, 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;
/* ... */
}
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 pode entrar no 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 um nome muito apropriado: são propriedades CSS que você pode nomear como quiser e aplicar o valor que for necessário. O único requisito é que eles tenham dois hifens como prefixo. 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);
}
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 comuns. 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 com 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 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 do Nord
Sempre que desenvolvemos um componente para nosso sistema de design, adotamos uma abordagem cuidadosa em relação ao CSS. Nosso objetivo é criar 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);
/* ... */
}
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);
/* ... */
}
Também vamos abstrair alguns valores específicos do componente, mas que não estão nos 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 ser mais "dry" 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);
}
Em segundo lugar, as mudanças de estado e variação do componente ficam muito limpas. Só a propriedade personalizada precisa ser alterada para atualizar todas as 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);
}
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>
O exemplo anterior mostra um dos nossos componentes da Web com uma propriedade personalizada contextual alterada usando um seletor. O resultado dessa abordagem é um componente que oferece flexibilidade de estilo suficiente para o usuário, mas mantém a maior parte dos estilos reais sob controle. Além disso, como 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 é extremamente eficiente, não apenas para nós, como criadores dos componentes do nosso sistema de design, mas também para nossa equipe de desenvolvimento, que usa esses componentes nos nossos produtos.
Indo além com as propriedades personalizadas
No momento, não revelamos essas propriedades personalizadas contextuais na nossa documentação. No entanto, planejamos fazer isso para que nossa equipe de desenvolvimento 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. Agora, por exemplo, se você quiser ajustar a cor de dois componentes divisores, precisará segmentar ambos 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 contêiner 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>
Você precisa definir o valor da propriedade personalizada diretamente no componente porque estamos definindo-as no mesmo elemento usando o 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 pai. Como podemos ter o melhor dos dois mundos?
Propriedades personalizadas particulares e públicas
As propriedades personalizadas particulares foram criadas por Lea Verou. Elas são propriedades personalizadas "particulares" contextuais no próprio 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);
/* ... */
}
Definir as propriedades personalizadas contextuais dessa maneira significa que ainda podemos fazer tudo o que fazíamos antes, como herdar valores de tokens globais e reutilizar valores em todo o código do componente. No entanto, o componente também vai herdar de forma otimizada 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>
Embora seja possível 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 nossa equipe de desenvolvimento tenha mais controle sobre o uso deles, sem deixar de se beneficiar das proteções que temos.
Espero que essa explicação sobre como usamos componentes da Web com propriedades personalizadas de CSS tenha sido útil. Diga 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, assim como o restante da minha equipe, que trabalhou duro para reunir esse sistema de design e executar os recursos mencionados neste documento: @Viljamis, @WickyNilliams e @eric_habich.