Back

Trabalhando com Variáveis CSS Tipadas Usando @property

Trabalhando com Variáveis CSS Tipadas Usando @property

A at-rule CSS @property atribui um tipo a uma propriedade customizada. Uma vez registrada, o browser valida cada atribuição, interpola entre valores durante animações e recorre a um valor inicial definido em caso de entrada inválida. O @property corrige as lacunas de validação e interpolação das variáveis CSS — mas altera o modo de falha de uma forma que a maioria dos desenvolvedores não espera, substituindo um valor visivelmente incorreto por um fallback silencioso que não gera nenhum erro.

Este artigo aborda os três descritores e quais são obrigatórios, o registro de tipos suportados com exemplos concretos, o comportamento de fallback silencioso e o que os usuários realmente veem quando ele é acionado, a animação de propriedades tipadas além da habitual demonstração de rotação, o equivalente em JavaScript com CSS.registerProperty(), o suporte atual dos browsers e um critério de decisão para quando o registro não vale a complexidade adicional.

Principais Conclusões

  • Uma propriedade customizada CSS não tipada é uma string; o @property atribui a ela um tipo que o browser valida em cada ponto de atribuição.
  • Os descritores syntax e inherits são sempre obrigatórios, e initial-value é obrigatório sempre que syntax não for "*" — conforme a especificação CSS Properties and Values API Level 1.
  • Quando uma propriedade registrada recebe um valor que não corresponde ao seu syntax declarado, o browser silenciosamente reverte para initial-value sem nenhum erro no console e sem nenhum indicador visual de que o fallback foi acionado.
  • Propriedades tipadas interpolam durante transições e animações; propriedades customizadas não tipadas não fazem isso, pois o browser as enxerga como duas strings opacas sem ponto médio numérico.
  • O @property está disponível como Baseline Newly Available desde 9 de julho de 2024 — as ressalvas sobre “experimental” em referências anteriores a 2024 estão obsoletas.

O problema: propriedades customizadas não tipadas são apenas strings

Uma propriedade customizada CSS padrão armazena uma string não processada até ser substituída em uma propriedade real. O browser não sabe se --accent deve ser uma cor, um comprimento ou uma palavra-chave. Ele não realiza nenhuma validação no ponto de declaração, não consegue interpolar entre dois valores durante uma animação e não fornece nenhum feedback quando um valor está estruturalmente errado para o uso pretendido.

Essa terceira lacuna é a mais relevante na prática. Considere uma propriedade não tipada usada em um text-shadow:

.card {
  --accent: red;
  text-shadow: 4px 2px 5px var(--accent);
}

/* em outro lugar, por engano */
.card {
  --accent: 20px;
}

A declaração text-shadow torna-se inválida no momento da substituição e a sombra desaparece. Não há nenhum aviso no ponto em que --accent foi definido como 20px, pois naquele momento ainda é apenas uma string. O browser não tem noção de que essa propriedade deveria ser uma cor. O guia de propriedades customizadas do MDN descreve este modelo de substituição: o valor de uma propriedade customizada é resolvido apenas quando referenciado via var().

O @property, proveniente da especificação CSS Properties and Values API Level 1, adiciona um tipo à própria propriedade. Uma vez registrado, o browser sabe que --accent é uma <color> e impõe isso em cada atribuição, não apenas na substituição.

Sintaxe: os três descritores e quais são obrigatórios

A at-rule @property aceita três descritores: syntax, inherits e initial-value. Os descritores syntax e inherits são sempre obrigatórios; initial-value é obrigatório sempre que syntax não for "*" — omiti-lo de um registro tipado torna todo o bloco @property inválido e ignorado.

@property --accent {
  syntax: "<color>";
  inherits: false;
  initial-value: #586de7;
}
  • syntax — uma string que descreve o tipo aceito, extraída de um conjunto fixo de nomes suportados definidos pela especificação (abordado na próxima seção).
  • inherits — um booleano (true ou false) que controla se a propriedade é herdada pela árvore DOM. Este é o mesmo comportamento de herança de qualquer propriedade CSS; defini-lo explicitamente é o que torna as propriedades tipadas previsíveis em componentes aninhados.
  • initial-value — o valor utilizado quando nenhum outro valor válido se aplica, e o valor para o qual a propriedade reverte em caso de entrada inválida.

A especificação CSS Properties and Values API Level 1, §3.1 define o requisito com precisão: uma regra @property é inválida se syntax ou inherits estiver ausente, e também é inválida se initial-value estiver ausente a menos que syntax seja o universal "*". Vários tutoriais existentes descrevem initial-value como incondicionalmente obrigatório; a especificação vincula isso ao valor de syntax, e inherits não tem relação com essa condição. Uma regra @property inválida é descartada — o registro simplesmente não acontece e a propriedade reverte ao comportamento não tipado.

O registro de tipos do CSS @property

O descritor syntax aceita os nomes de componentes de sintaxe suportados definidos pela especificação CSS Properties and Values API Level 1, §2 — incluindo <color>, <length>, <percentage>, <integer>, <angle>, <image> e <custom-ident> — além de multiplicadores (+ para listas separadas por espaço, # para listas separadas por vírgula) e sintaxe de união (<color> | <length>) para propriedades que legitimamente aceitam múltiplos tipos. Esta é uma lista fixa de nomes suportados, não uma abertura para qualquer palavra-chave de tipo CSS.

Valor de syntaxAceitaRejeitaExemplo de initial-value
"<color>"qualquer cor válida (#f00, rebeccapurple, oklch(...))comprimentos, palavras-chave como darkpink#586de7
"<length>"px, rem, em, vw, etc.números sem unidade, porcentagens20px
"<percentage>"50%comprimentos, números sem unidade100%
"<integer>"números inteiros (12)1.5, comprimentos12
"<angle>"deg, rad, turn, gradnúmeros sem unidade0deg
"<image>"url(...), gradientescores, comprimentosurl(bg.png)
"<custom-ident>"identificadores definidos pelo autornúmeros, strings entre aspasnone
"*"qualquer valor (passagem não tipada)nada — aceita tudoopcional

Três extensões de gramática ampliam o que uma única propriedade aceita:

/* "+" — uma lista de comprimentos separados por espaço */
@property --insets {
  syntax: "<length>+";
  inherits: false;
  initial-value: 0px;
}

/* "#" — uma lista de cores separadas por vírgula */
@property --stops {
  syntax: "<color>#";
  inherits: false;
  initial-value: black;
}

/* "|" — uma união: aceita tanto um comprimento quanto a palavra-chave "auto" */
@property --gap {
  syntax: "<length> | auto";
  inherits: false;
  initial-value: auto;
}

O multiplicador + indica uma lista separada por espaço; # indica uma lista separada por vírgula, conforme a seção de strings de sintaxe suportadas da especificação. A união | permite que uma propriedade aceite mais de um tipo — útil para propriedades que genuinamente aceitam, por exemplo, um comprimento ou uma palavra-chave. A sintaxe universal "*" desativa completamente a verificação de tipos; é o único caso em que initial-value é opcional, pois não há tipo para definir como padrão. Para definições por tipo, a referência de tipos de valores CSS do MDN e o índice de tipos do CSSWG listam cada nome de componente.

Validação: o fallback silencioso que você não espera

Quando uma propriedade registrada recebe um valor que não corresponde ao seu syntax declarado, o browser descarta a atribuição e renderiza o elemento usando initial-value. Nos browsers atuais, esse fallback não produz nenhum erro no console e nenhuma indicação visual na página renderizada de que o fallback foi acionado — a página não quebra, mas também não informa que algo deu errado.

@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 90deg;
}

.card {
  --hue: 220deg;            /* ✅ válido, utilizado */
  --hue: #f00;              /* ❌ tipo inválido, ignorado — reverte para 90deg */
  background: oklch(70% 0.15 var(--hue));
}

O background sempre resolve para uma cor válida. Após a atribuição inválida, --hue não se torna #f00 nem fica vazio — o valor inválido é descartado e a propriedade resolve para seu initial-value registrado de 90deg. A página do MDN sobre @property documenta isso como a propriedade tornando-se “inválida no momento do valor computado”, que resolve para o valor inicial registrado.

Isso é genuinamente melhor do que um layout visivelmente quebrado. Mas também representa uma nova classe de falha. Propriedades customizadas não tipadas falham de forma visível — a declaração dependente quebra e você percebe. Propriedades tipadas falham silenciosamente: um alternador de temas em JS escreve uma cor malformada, um valor fornecido pelo usuário não é processado corretamente, um token de design recebe a unidade errada, e o componente renderiza em seu estado padrão sem nenhum rastro de erro. O DevTools mostra o valor de fallback computado se você inspecionar o elemento, mas nada aparece no console em tempo de execução.

Esta é a classe de bug para a qual o session replay foi criado. Quando uma propriedade customizada tipada recebe entrada inválida em produção — proveniente de input do usuário, um token mal configurado ou uma mudança de tema em tempo de execução — o browser silenciosamente recorre ao initial-value, nenhum erro JavaScript é disparado e o monitoramento de erros padrão não produz nenhum sinal. A única evidência pós-implantação é visual: um componente renderizado com a cor ou o tamanho errado. Replays de sessão dessas implementações frequentemente revelam o estado incorreto diretamente, capturando o DOM renderizado no momento em que o valor inválido foi atribuído, onde uma ferramenta baseada apenas no console não detecta nada.

Animação: interpolando propriedades tipadas

Propriedades customizadas registradas interpolam durante animações; as não registradas animam de forma discreta. Esta é a consequência mais útil da tipagem. Como o browser entende que --hue é um <angle> e não uma string, ele pode interpolar entre 0deg e 360deg ao longo de uma transição — algo impossível com uma propriedade customizada não tipada, onde o browser enxerga duas strings opacas sem ponto médio numérico. A especificação CSS Transitions define a interpolação como operando sobre valores tipados; uma propriedade customizada não registrada não tem tipo, então o browser a troca de forma discreta em vez de fazer a transição gradual.

Todo outro tutorial demonstra isso com transform: rotate(). Aqui está um caso mais ilustrativo — animando o canal de matiz de uma cor oklch(), que mostra que a tipagem permite interpolar um valor dentro de uma função, não apenas uma propriedade independente:

@property --hue {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.swatch {
  width: 200px;
  height: 200px;
  border-radius: 12px;
  background: oklch(65% 0.2 var(--hue));
  animation: hue-cycle 6s linear infinite;
}

@keyframes hue-cycle {
  to {
    --hue: 360deg;
  }
}

O swatch percorre suavemente toda a roda de matizes porque o browser faz a transição de --hue de 0deg a 360deg e recalcula oklch(65% 0.2 var(--hue)) a cada frame. A especificação CSS Color Level 4 define o argumento de matiz de oklch() como aceitando um <angle>, que é exatamente o que registramos. Remova o bloco @property e a animação quebra: --hue torna-se uma string não tipada, o browser não consegue interpolá-la e o swatch salta do início ao fim em vez de percorrer o ciclo. Esse antes/depois é a demonstração mais clara de por que o registro importa para motion.

O equivalente em JavaScript: CSS.registerProperty()

CSS.registerProperty() é o equivalente imperativo da at-rule @property. Ele registra uma propriedade customizada tipada em tempo de execução a partir do JavaScript, recebendo um objeto com name, syntax, inherits e um initialValue opcional:

window.CSS.registerProperty({
  name: "--hue",
  syntax: "<angle>",
  inherits: false,
  initialValue: "0deg",
});

Observe o initialValue em camelCase na API JS versus o descritor hifenizado initial-value no CSS. A referência do MDN para CSS.registerProperty() documenta os nomes dos parâmetros e o comportamento. Os dois caminhos de registro são equivalentes em efeito; uma propriedade registrada por qualquer um dos métodos é tipada e validada de forma idêntica.

Use a at-rule por padrão — ela fica junto com o restante dos seus estilos, é declarativa e não requer execução de JavaScript para ter efeito. Use CSS.registerProperty() quando o registro precisar ser dinâmico: uma propriedade cujo syntax ou initialValue depende de condições em tempo de execução, ou uma biblioteca que registra propriedades programaticamente como parte de sua inicialização. Observe que uma propriedade registrada com CSS.registerProperty() não pode ser re-registrada, portanto, proteja-se contra executá-la duas vezes.

Suporte dos browsers

A partir de 9 de julho de 2024, o @property está disponível como Baseline Newly Available — suportado nas versões atuais do Chrome, Firefox e Safari — tornando obsoletas as ressalvas sobre “experimental” em referências mais antigas. O Firefox adicionou suporte na versão 128, lançada em julho de 2024, o que completou o suporte entre browsers; o Safari implementou na versão 16.4; o Chrome oferece suporte desde a versão 85. O anúncio Baseline do web.dev confirma a data e o status. Para dados exatos de versão, consulte o caniuse. Tutoriais publicados antes de meados de 2024 descrevem o suporte como “experimental” ou “em breve”; essas afirmações não são mais válidas.

Padrões do mundo real

Os usos de maior valor do @property compartilham uma característica: a propriedade ou anima, recebe entrada externa ou precisa de controle explícito de herança.

Definição global, consumo com escopo

Defina os blocos @property uma vez em uma camada global de tokens; os componentes consumidores referenciam a variável com var() como de costume. Todo tutorial de referência declara @property diretamente acima da regra que o utiliza, o que é enganoso para trabalho com design systems — o padrão realista separa o registro do consumo:

/* tokens.css — carregado uma vez na raiz do documento */
@property --brand-hue {
  syntax: "<angle>";
  inherits: true;
  initial-value: 250deg;
}

@property --surface {
  syntax: "<color>";
  inherits: true;
  initial-value: #1a1a1a;
}
/* card.css — o componente nunca referencia o registro */
.card {
  background: var(--surface);
  border-color: oklch(60% 0.1 var(--brand-hue));
}

Qualquer atribuição a --surface — de um alternador de temas, uma media query ou input do usuário — é validada. Definir inherits: true permite que os tokens se propaguem em cascata para os descendentes.

Tematização de cores com superfícies adaptativas

Tokens de cor tipados permitem que um único --brand-hue conduza uma paleta de superfícies através de oklch(); um valor de matiz malformado recorre ao valor inicial registrado em vez de quebrar a paleta:

html:has(#dark:checked) {
  --surface: oklch(20% 0.1 var(--brand-hue));
}
html:has(#light:checked) {
  --surface: oklch(95% 0.04 var(--brand-hue));
}

Indicadores de progresso baseados em scroll

Um <percentage> ou <length> tipado é lido de forma limpa como um valor de progresso e interpola suavemente quando acionado por uma animação ou atualizado a partir do JavaScript:

@property --progress {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

.progress-bar {
  width: var(--progress);
  transition: width 0.2s linear;
}
function onScroll() {
  const pct = (scrollY / (document.body.scrollHeight - innerHeight)) * 100;
  document.querySelector(".progress-bar")
    .style.setProperty("--progress", `${pct}%`);
}

Tipar --progress como <percentage> significa que um valor não percentual acidental reverte para 0% em vez de corromper width.

Quando não usar @property

Pule o registro com @property para propriedades customizadas que nunca animam, nunca recebem entrada externa e não precisam de controle explícito de herança. Os três descritores adicionam sobrecarga sintática sem nenhum benefício em tempo de execução para tokens puramente estáticos. O registro se justifica quando pelo menos uma das três condições se aplica:

  1. O valor anima ou faz transição. A interpolação requer um tipo registrado.
  2. O valor recebe entrada externa que pode ser inválida. Um alternador de temas, input do usuário ou um token de build-time que pode estar malformado se beneficia da garantia de fallback silencioso.
  3. O comportamento de herança precisa de controle explícito. Quando você precisa fixar o comportamento de cascata de uma propriedade em componentes aninhados.

Para uma escala de espaçamento estática, uma hierarquia de z-index ou uma string font-family que é definida uma vez e nunca animada ou alimentada com entrada externa, uma propriedade customizada simples em :root é mais simples e resolve o problema. Adicionar @property nesses casos resulta em três descritores para manter sem nenhum comportamento adicional. Tipe as propriedades que se movem ou recebem entrada; deixe o restante como strings.

Propriedades customizadas tipadas transformam o browser em um validador e um motor de interpolação, mas a contrapartida é um modo de falha silencioso: entrada inválida reverte para initial-value sem rastro de erro. Registre as propriedades que animam ou recebem entrada em tempo de execução, defina valores iniciais sensatos e trate esse fallback como um comportamento a ser monitorado em produção, e não como uma rede de segurança que oculta problemas.

Perguntas Frequentes

A at-rule @property é uma at-rule de nível superior e é declarada de forma independente, não aninhada dentro de :root ou de qualquer seletor. Você escreve o bloco @property em qualquer lugar da sua folha de estilos para registrar o tipo globalmente, e então define o valor da propriedade dentro de :root ou de qualquer seletor como faria com qualquer propriedade customizada. O registro se aplica a todo o documento independentemente de onde o valor for atribuído posteriormente, portanto, colocar @property em um arquivo global de tokens e atribuir valores em :root é o padrão recomendado.

Ambos registram uma propriedade customizada tipada com comportamento em tempo de execução idêntico, mas @property é CSS declarativo que entra em vigor sem JavaScript, enquanto CSS.registerProperty() é executado imperativamente em tempo de execução. Use @property por padrão, pois ele fica junto com seus estilos e não requer execução de script. Recorra a CSS.registerProperty() apenas quando o registro precisar ser dinâmico, como quando syntax ou initialValue depende de condições em tempo de execução. Observe que CSS.registerProperty() usa camelCase initialValue, não pode re-registrar uma propriedade e lança um erro se chamado duas vezes para o mesmo nome.

Não. Uma propriedade customizada não tipada é armazenada como uma string opaca, então o browser enxerga duas strings sem ponto médio numérico e troca o valor de forma discreta em vez de interpolar. Registrar a propriedade com @property atribui a ela um tipo que o browser compreende, o que habilita a interpolação em transições e keyframes. Por exemplo, um ângulo não registrado salta do início ao fim, enquanto a mesma propriedade registrada como <angle> faz a transição suavemente. O registro de tipo é o que torna a interpolação possível.

O browser descarta a atribuição inválida e renderiza o elemento usando o initial-value registrado. Isso acontece silenciosamente, sem nenhum erro no console e sem nenhuma indicação visual na página renderizada de que o fallback foi acionado — comportamento descrito na especificação como tornar-se inválido no momento do valor computado. A página não quebra, mas também não sinaliza que algo deu errado, o que torna essas regressões invisíveis para o monitoramento de erros padrão. O DevTools mostra o valor de fallback computado apenas se você inspecionar o elemento diretamente.

Truly understand users experience

See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..

OpenReplay