Skip to content
This page has been auto-translated and may contain errors.View in English

Propriedades customizadas e CSS moderno

docs.scrimba.com

Você escolhe um azul da marca, digita em quarenta regras diferentes e envia o site. Um mês depois o marketing quer um azul ligeiramente diferente. Agora você está vasculhando todas as folhas de estilo, esperando pegar os quarenta e não perder os dois que foram escritos com letra maiúscula. Propriedades customizadas resolvem isto: você nomeia a cor uma vez, usa o nome em todo lugar, e muda em um único lugar. Esse hábito é o começo de tematização, design tokens e folhas de estilo que sobrevivem a um redesenho.

Declarando e usando uma variável

Uma propriedade customizada é um valor que você dá um nome para poder reutilizar. Você escreve o nome com dois traços na frente, define para um valor, e depois o lê com var():

css
:root {
  --color-brand: #2563eb;   /* declare o valor uma vez, sob um nome */
}

.button {
  background: var(--color-brand);   /* leia aqui */
}

.link {
  color: var(--color-brand);        /* e aqui, mesmo valor */
}

--color-brand é o nome (os dois traços são obrigatórios), e var(--color-brand) é como você o usa. Declarar em :root (que é o elemento <html>, o topo da página) torna o nome disponível em todos os lugares. Mude a única linha em :root e cada regra que a lê se atualiza.

Uma propriedade customizada é um valor nomeado que você declara com um prefixo -- e relê com a função var(). As pessoas também chamam de variáveis CSS, e os dois nomes significam a mesma coisa:

css
:root {
  --color-brand: #2563eb;
  --color-brand-dark: #1e40af;
}

.button {
  background: var(--color-brand);
}

.button:hover {
  background: var(--color-brand-dark);
}

O prefixo -- é o que separa uma propriedade customizada de uma propriedade normal, e é sensível a maiúsculas, então --color-Brand e --color-brand são dois nomes diferentes. Declarar em :root é a escolha comum porque coloca o valor no topo da árvore onde cada elemento pode vê-lo, que é onde seus tokens globais devem estar. Você não está limitado a cores: qualquer valor que uma propriedade aceite pode viver em uma propriedade customizada, incluindo comprimentos, sombras e pilhas de fontes.

Uma propriedade customizada é uma propriedade definida pelo usuário cujo nome começa com --, contendo um valor que você relê através da função var(). O nome é sensível a maiúsculas e o valor é armazenado quase integralmente: o navegador faz muito pouco com ele no momento da declaração, o que se torna importante quando você vê onde ele realmente se resolve.

css
:root {
  --color-brand: #2563eb;
  --space-md: 16px;
  --shadow-card: 0 1px 3px rgb(0 0 0 / 0.12);
}

.card {
  padding: var(--space-md);
  box-shadow: var(--shadow-card);
}

Declarar em :root é uma convenção, não um requisito. :root corresponde ao elemento <html> com uma especificidade ligeiramente maior que o seletor de tipo html, e porque propriedades customizadas herdam, um valor definido lá é visível para cada descendente. Esse é o modelo mental a manter: você não está criando um registro global, você está definindo uma propriedade no elemento raiz que acontece de herdar para o documento inteiro. Qualquer coisa que uma propriedade normal aceite pode ser armazenada, e você também armazenará fragmentos de valores (um canal de cor, um número adimensional) e os montará depois.

JunoDeclarando e usando uma variável Uma propriedade customizada é um valor com um nome: escreva com dois traços como --color-brand, depois leia com var(--color-brand). Coloque em :root para que toda a página possa usá-la. Mude a única linha e cada regra que lê o nome se atualiza com ela.
JunoDeclarando e usando uma variável O prefixo -- marca uma propriedade customizada, e var() a relê. Declarar em :root coloca seus tokens globais no topo da árvore onde cada elemento pode alcançá-los. Nomes são sensíveis a maiúsculas, e qualquer valor que uma propriedade aceite pode viver em uma, não apenas cores.
JunoDeclarando e usando uma variável Uma propriedade customizada é qualquer --name que você define, relida através de var(), armazenada quase integralmente até que se resolva. :root é uma convenção: é o elemento <html>, e porque essas propriedades herdam, um valor lá alcança cada descendente. Você também pode armazenar valores parciais e montá-los depois, o que a seção de fallback começa a usar.

Elas em cascata e herdam

Se você usou variáveis em outra linguagem, essa próxima parte é a surpresa: uma propriedade customizada não é fixa. Ela segue as mesmas regras que qualquer outra propriedade CSS, o que significa que ela herda para elementos filhos, e você pode dar a ela um valor diferente em um lugar sem tocar no resto.

css
:root {
  --color-text: #1f2937;   /* o padrão para a página inteira */
}

.callout {
  --color-text: #92400e;   /* um valor diferente, apenas dentro de .callout */
  color: var(--color-text);
}

p {
  color: var(--color-text);   /* cinza escuro normalmente, âmbar dentro de um callout */
}

O mesmo var(--color-text) fornece cinza escuro na maioria dos parágrafos mas âmbar dentro de um callout, porque o callout definiu seu próprio valor. Isto é o que torna a tematização possível, e você viu a cascata por trás disso em como CSS funciona.

Aqui é onde uma propriedade customizada se separa de uma variável de um pré-processador CSS como Sass. Uma variável de pré-processador é compilada antes do navegador a ver, então é uma substituição estática encontrar-e-substituir. Uma propriedade customizada é uma propriedade CSS real, viva no navegador, então ela obedece a cascata e herda.

O valor de uma propriedade customizada é procurado no elemento que o lê, não onde foi declarado. Esse único fato é por que a tematização de componentes funciona:

css
:root {
  --color-accent: #2563eb;   /* azul em todo lugar por padrão */
}

.card--warning {
  --color-accent: #d97706;   /* override para essa subárvore de componente */
}

.card__badge {
  background: var(--color-accent);   /* azul, ou âmbar dentro de um card de aviso */
}

O emblema não sabe ou se importa de onde --color-accent veio. Ele lê o valor em vigor sobre si mesmo, e qualquer ancestral pode tê-lo redefinido. Você override o token em um container e cada descendente que o lê redesenha, sem editar uma única regra de componente. As regras completas de cascata e herança vivem em como CSS funciona.

O comportamento que separa propriedades customizadas de toda variável de pré-processador é que elas participam da cascata e herdam por padrão. Uma variável Sass ou Less é resolvida em tempo de compilação e desaparece antes do navegador carregar o arquivo. Uma propriedade customizada é uma propriedade real no elemento, resolvida pela mesma cascata que governa color ou font-size, o que significa que var() é procurada contra o valor calculado da propriedade no elemento fazendo a leitura.

css
:root {
  --color-accent: #2563eb;
}

.panel[data-tone="warning"] {
  --color-accent: #d97706;   /* definido na raiz da subárvore */
}

.panel__button {
  background: var(--color-accent);   /* resolvido por elemento, herda do painel */
}

Duas consequências que valem a pena manter. Primeiro, herança faz a tematização: definir --color-accent em uma raiz de subárvore se propaga para cada descendente que a lê, então um override de container redesenha uma árvore de componente inteira sem mudanças em suas regras. Segundo, especificidade ainda se aplica a onde o token é definido, não a var() em si. Se dois seletores ambos definem --color-accent no mesmo elemento, a cascata usual decide qual vence, exatamente como seria para qualquer propriedade. Uma propriedade que deveria parar de herdar pode ser resetada com initial, ou declarada como não herdando com @property, que a seção de deep dive abaixo alcança. As regras de cascata se aplicam aqui sem exceção.

JunoElas em cascata e herdam Uma propriedade customizada não é congelada: ela herda para elementos filhos, e você pode definir um valor diferente em um ponto sem mudar o resto. É por isso que o mesmo var(--color-text) pode ser cinza na maioria dos parágrafos e âmbar dentro de um callout. Redefina o nome em um container e tudo dentro dele pega o novo valor.
JunoElas em cascata e herdam Ao contrário de uma variável Sass que desaparece em tempo de compilação, uma propriedade customizada é viva no navegador e segue a cascata, então seu valor é lido no elemento que a usa. Override um token em um container e cada descendente redesenha, com zero edições nas regras do componente. Esse comportamento de procura-onde-você-lê é todo o truque por trás da tematização.
JunoElas em cascata e herdam Propriedades customizadas herdam e em cascata, o que uma variável de pré-processador nunca faz porque desaparece antes do navegador carregar. var() se resolve contra o valor calculado no elemento leitor, então definir um token em uma raiz de subárvore tematiza tudo abaixo através apenas de herança. Especificidade governa onde o token é definido, não a chamada var() em si.

Fallbacks e calc()

var() pode receber um segundo valor, usado apenas se a propriedade customizada nunca foi definida. Este é o fallback, e mantém uma regra funcionando mesmo quando um token está faltando:

css
.card {
  gap: var(--gap, 1rem);   /* use --gap se existir, caso contrário 1rem */
}

Você também pode fazer matemática com uma propriedade customizada usando calc(). Se você tem um valor de espaçamento base, você pode construir um maior a partir dele em vez de hard-coding um segundo número:

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, derivado da base */
}

Derivar valores dessa forma significa uma mudança para --space-md se propaga através de tudo construído em cima dele.

var() aceita um segundo argumento que age como um fallback: var(--gap, 1rem) se resolve para --gap se estiver definido, e para 1rem se não estiver. Esse fallback vale a pena usar em qualquer token que possa não ser definido em uma determinada subárvore, então um componente ainda renderiza sensatamente quando colocado em algum lugar onde seus tokens não foram fornecidos.

css
.card {
  gap: var(--gap, 1rem);
  padding: var(--space-md, 16px);
}

Propriedades customizadas chegam ao seu apogeu com calc(), porque é assim que você deriva um valor de outro em vez de armazenar um monte de números mágicos:

css
:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);        /* 32px */
  margin-bottom: calc(var(--space-md) * 3);  /* 48px, uma única fonte da verdade */
}

Uma coisa para observar: o fallback pode ser um valor completo incluindo vírgulas, então var(--shadow, 0 1px 2px black) é válido, e tudo após a primeira vírgula é tratado como o fallback. Veja unidades de dimensionamento para como as unidades dentro de calc() se resolvem.

var() recebe um fallback opcional como seu segundo argumento: var(--gap, 1rem) rende 1rem quando --gap não está definido no elemento leitor (ou está definido para o valor garantidamente-inválido). O fallback gananciosamente pega tudo após a primeira vírgula, então pode ser a si mesmo um valor contendo vírgulas ou um var() aninhado, o que permite a você encadear padrões: var(--gap, var(--space-md, 1rem)).

css
.card {
  gap: var(--gap, var(--space-md, 1rem));   /* fallback encadeado */
}

:root {
  --space-md: 16px;
}

.section {
  padding: calc(var(--space-md) * 2);   /* 32px, derivado não hard-coded */
}

O emparelhamento com calc() é onde propriedades customizadas ganham seu lugar em um sistema de escala, e vem com uma regra de precisão. Uma propriedade customizada pode conter um número adimensional, e você multiplica uma unidade nele dentro de calc():

css
:root {
  --scale: 1.5;   /* adimensional, então pode ser multiplicado por qualquer unidade */
}

.title {
  font-size: calc(1rem * var(--scale));   /* 1.5rem */
  line-height: calc(var(--scale) + 0.2);  /* aritmética de número puro */
}

A razão de você armazenar --scale sem unidade é que calc(var(--scale) * 16px) funciona, enquanto armazenar --scale: 1.5rem e tentar calc(var(--scale) * 16px) multiplica dois comprimentos e é inválido. Mantenha razões brutas adimensionais e anexe a unidade no ponto de uso. Como essas unidades se resolvem para pixels é coberto em unidades de dimensionamento.

JunoFallbacks e calc()var(--gap, 1rem) usa o token se existir e cai de volta para 1rem se não, então uma regra continua funcionando quando um valor está faltando. E calc() permite a você construir valores a partir de um token, como calc(var(--space-md) * 2) para o dobro do espaçamento base. Mude a base e tudo construído nela segue.
JunoFallbacks e calc() O segundo argumento para var() é um fallback: var(--gap, 1rem) cobre o caso onde o token nunca foi definido. Emparelhe propriedades customizadas com calc() para derivar valores em vez de hard-coding números mágicos, então um token impulsa a escala inteira. Note que o fallback engole tudo após a primeira vírgula, o que é útil para valores de múltiplas partes.
JunoFallbacks e calc()var(--gap, 1rem) cai de volta gananciosamente, então você pode encadear padrões como var(--gap, var(--space-md, 1rem)). Armazene razões brutas adimensionais, porque calc(var(--scale) * 16px) funciona enquanto multiplicar dois comprimentos não, e anexe a unidade no ponto de uso. Isto mantém um token impulsionando uma escala inteira de tipo ou espaçamento.

Tematização

Porque uma propriedade customizada pode ser redefinida mais abaixo na árvore, você pode construir um tema escuro mudando os tokens sob uma classe, e deixar cada regra de componente sozinha:

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
}

.theme-dark {
  --color-bg: #0f172a;      /* mesmos nomes, novos valores */
  --color-text: #e2e8f0;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
}

Adicione class="theme-dark" à página e cada regra que lê --color-bg ou --color-text muda para os valores escuros. Você nunca tocou na regra body, ou em nenhum componente, para fazer a troca de tema. Esse é o retorno de nomear suas cores como tokens em vez de escrever os códigos hex diretamente.

Tematização é a recompensa direta de tokens que em cascata. Você define suas cores uma vez como tokens, escreve cada componente contra os nomes dos tokens, e então um tema é nada mais que um segundo conjunto de valores para esses mesmos nomes sob uma classe ou uma media query:

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

Alternar theme-dark em um container redesenha tudo dentro dele, porque os componentes leem nomes de tokens, não cores fixas. O mesmo funciona com uma media query então o tema segue a configuração do sistema operacional:

css
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

A disciplina que torna isto funcionar é manter códigos hex brutos fora das regras de componentes inteiramente, e nomeá-los como tokens de cor em vez. Isto é também onde uma convenção de nomeação de tokens paga, o que organizar CSS entra.

Tematização é a razão pela qual propriedades customizadas importam para mais do que reuso de um único valor, e funciona porque um tema é nada mais que um override de cascata de sua camada de token. Componentes leem nomes de tokens; um tema redefine esses nomes em um ponto mais alto na árvore. Nada na camada de componente sabe que um tema existe.

css
:root {
  --color-bg: #ffffff;
  --color-text: #1f2937;
  --color-surface: #f8fafc;
}

.theme-dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-surface: #1e293b;
}

@media (prefers-color-scheme: dark) {
  :root:not(.theme-light) {
    --color-bg: #0f172a;
    --color-text: #e2e8f0;
    --color-surface: #1e293b;
  }
}

O padrão que mantém em produção é um sistema de token de duas camadas. Uma primeira camada mantém valores brutos (--blue-600: #2563eb), e uma segunda camada mantém tokens semânticos que os referenciam (--color-accent: var(--blue-600)). Componentes leem apenas a camada semântica, então um tema reatribui os tokens semânticos e a camada de paleta permanece, o que mantém a troca pequena e a intenção legível. Emparelhando um override de classe com a media query prefers-color-scheme, como acima, oferece a você um padrão do sistema mais um toggle manual sem duplicar regras de componente. A disciplina de nomeação que mantém isto mantível pertence ao resto de sua organização de arquivo e token, e os valores brutos em si são cores.

Três coisas mais pertencem a uma visualização avançada de propriedades customizadas, porque mudam o que é possível em vez de adicionar polimento.

Propriedades customizadas se resolvem em tempo de valor computado, o que significa que o navegador resolve var() por elemento enquanto calcula estilos, não quando o arquivo é analisado. Isto é exatamente por que tematização funciona e por que uma variável de pré-processador não pode fazer: o valor é vivo, então ele responde à cascata, à herança, e a mudanças feitas em tempo de execução.

Tempo de execução é o segundo ponto. Porque essas são propriedades reais, JavaScript pode lê-las e escrevê-las, e cada regra lendo o token se atualiza instantaneamente:

css
:root {
  --sidebar-width: 280px;   /* lida e definida de JS em tempo de execução */
}

Um script pode chamar document.documentElement.style.setProperty('--sidebar-width', '320px') e o layout reflui contra o novo valor, algo que uma variável Sass, compilada muito antes da página carregar, nunca pode oferecer. Você também pode escopear isto para uma subárvore definindo a propriedade em qualquer elemento em vez da raiz, que confina uma mudança para aquele ramo da árvore.

O terceiro ponto é controle sobre a propriedade em si. @property permite que você registre uma propriedade customizada com um tipo, um valor inicial, e se ela herda, o que desbloqueia animação suave de propriedades customizadas (uma propriedade customizada simples anima como um salto liga/desliga porque o navegador não pode interpolar um valor sem tipo). Junto a ele, as funções de valor moderno leem naturalmente de tokens: clamp(min, preferred, max) escolhe o valor preferido mas nunca deixa os limites, min() retorna o menor de seus argumentos e max() o maior, então width: min(100%, var(--max-width)) limita uma largura fluida a um token sem uma media query.

JunoTematização Um tema é os mesmos nomes de token com valores diferentes sob uma classe como .theme-dark. Defina class="theme-dark" e cada regra lendo esses tokens muda, sem editar um único componente. É por isso que você nomeia suas cores como tokens em vez de digitar o código hex direto em cada regra.
JunoTematização Porque tokens em cascata, um tema é apenas um segundo conjunto de valores para os mesmos nomes, sob uma classe ou uma media query prefers-color-scheme. Mantenha hex bruto fora de regras de componentes e leia nomes de tokens em vez, e uma subárvore inteira redesenha a partir de um override. Alterne a classe ou combine com a configuração do SO e tudo lendo esses tokens segue.
JunoTematização Propriedades customizadas se resolvem em tempo de valor computado, então são vivas: JavaScript pode definir --sidebar-width em tempo de execução e cada regra lendo-a reflui, o que nenhuma variável de pré-processador pode fazer. Uma configuração de duas camadas, tokens de paleta bruta sob tokens semânticos, mantém uma troca de tema para um punhado de reatribuições. E @property mais clamp(), min(), e max() deixam esses tokens animar e ficar limitados sem uma media query.