Aguarde...

31 de julho de 2024

Fornecendo definições de tipo para CSS com @property

Fornecendo definições de tipo para CSS com @property

Um recurso entre navegadores a partir do lançamento do Firefox 128 em julho de 2024 é uma nova regra – @property que permite definir tipos, bem como herança e um valor inicial para suas propriedades personalizadas.

Aprenderemos quando e por que os valores de fallback tradicionais podem falhar e como @propertyos recursos nos permitem escrever definições de propriedades personalizadas CSS mais seguras e resilientes.

Propriedades personalizadas – também conhecidas como “variáveis ​​CSS” – são úteis porque permitem criar referências a valores semelhantes a variáveis ​​em outras linguagens de programação.

Considere o cenário a seguir que cria e atribui a --color-blue propriedade, que então é implementada como uma classe e aplicada a um parágrafo. CSS para “Caso de uso padrão para propriedades personalizadas”

:root {
  --color-blue: blue;
}

.color-blue {
  color: var(--color-blue);
}

Eu sou dabadee azul

O parágrafo é renderizado como azul. Excelente! Envie.

Condições de erro comuns usando propriedades personalizadas

Agora, você e eu sabemos que “azul” é uma cor. E também pode parecer óbvio que você só aplicaria a classe que usa essa cor ao texto que pretendemos que seja azul .

Mas, o mundo real não é perfeito, e às vezes os consumidores downstream do nosso CSS acabam tendo um motivo para redefinir o valor. Ou talvez eles acidentalmente introduzam um erro de digitação que impacta o valor original.

O resultado de qualquer um desses cenários poderia ser:

  • o texto acaba ficando com uma cor além do azul, como o autor pretendia
  • o texto surpreendentemente fica preto

Se a cor renderizada for surpreendentemente preta, é provável que tenhamos atingido o cenário único de valor inválido no momento do cálculo .

Quando o navegador estiver avaliando as regras CSS e calculando qual valor aplicar a cada propriedade com base na cascata, herança, especificidade e assim por diante, ele manterá uma propriedade personalizada como o valor vencedor, desde que entenda a maneira geral como ela está sendo usada.

No nosso --color-blue exemplo, o navegador definitivamente entende a color propriedade, então ele assume que tudo ficará bem com o uso da variável também.

Mas o que acontece se alguém redefinir --color-blue para uma cor inválida? CSS para “Definir uma cor inválida”

:root {
  --color-blue: blue;
}

.color-blue {
  color: var(--color-blue);
}

p {
  --color-blue: notacolor;
}

Eu sou dabadee azul (talvez)

Opa, surpreendentemente está ficando preto.

Por que os fallbacks tradicionais podem falhar

Ok, antes de aprendermos o que essa frase assustadora significa, vamos dar uma olhada no DevTools e ver se ele nos dá uma pista sobre o que está acontecendo.

O painel Estilos no DevTools mostra .color-blue e a regra de parágrafo, sem erro aparente

Isso parece bastante normal e não parece revelar que algo está errado, tornando a solução de problemas muito mais complicada.

Você pode saber que propriedades personalizadas permitem um valor de fallback como segundo parâmetro, então talvez isso ajude! Vamos tentar. CSS para “Tentativa de resolução com fallback de propriedade personalizada”

:root {
  --color-blue: blue;
}

.color-blue {
  color: var(--color-blue, blue);
}

p {
  --color-blue: notacolor;
}

Eu sou dabadee azul (talvez)

Infelizmente, o texto ainda é renderizado em preto.

Ok, mas nosso bom amigo cascade existe, e antigamente costumávamos colocar coisas como propriedades prefixadas de vendor antes das não prefixadas. Então, talvez se usarmos um método similar e fornecermos uma colordefinição extra antes daquela que tem a propriedade customizada, podemos voltar para isso? CSS para “Tentativa de resolução com definição de cor extra”

:root {
  --color-blue: blue;
}

.color-blue {
  color: blue;
  color: var(--color-blue);
}

p {
  --color-blue: notacolor;
}

Eu sou dabadee azul (talvez)

Pena que não parece que estamos fazendo progresso na prevenção desse problema.

Isso ocorre devido ao cenário (que parece um pouco assustador) inválido no tempo de valor computado .

Embora o navegador tenha mantido nossa definição que espera um valor de propriedade personalizado, só mais tarde ele tenta realmente calcular esse valor.

Neste caso, ele olha para a .color-blue classe e o valor fornecido para a pregra do elemento e tenta aplicar o valor computado de notacolor. Neste estágio, ele descartou o valor alternativo de blue originalmente fornecido pela classe. Consequentemente, como notacoloris de fato não é uma cor e, portanto, inválido , o melhor que ele pode fazer é usar:

  • um valor herdado se a propriedade tiver permissão para herdar e um ancestral tiver fornecido um valor; ou
  • valor inicial conforme definido na especificação CSS

Enquanto color é uma propriedade herdável, não a definimos em nenhum ancestral, então a cor renderizada black é devido ao valor inicial .

Consulte este artigo anterior do Modern CSS sobre como valores de propriedades personalizadas são computados e aprenda mais profundamente sobre a condição de inválido no momento do valor computado.

Definindo tipos para CSS mais seguro

É hora de apresentar @property para ajudar a resolver esse problema do que você pode perceber como um valor renderizado surpreendente.

Os recursos críticos @property fornecidos são:

  • definindo tipos aceitáveis ​​para propriedades personalizadas específicas
  • habilitando ou desabilitando herança
  • fornecer um valor inicial como um mecanismo de segurança para valores inválidos ou indefinidos

Essa regra é definida por propriedade personalizada, o que significa que é necessária uma definição exclusiva para cada propriedade para a qual você deseja aproveitar esses benefícios.

Não é um requisito, e você certamente pode continuar usando propriedades personalizadas sem nunca trazer o @property.

Observe que, no momento em que este artigo foi escrito, @property ele é compatível com vários navegadores e eu recomendo que você o considere uma melhoria progressiva para beneficiar os usuários no suporte a navegadores.

Vamos aplicá-lo ao nosso dilema azul e ver como ele corrige o problema da cor inválida fornecida na regra do elemento. CSS para “Aplicar @propriedade a –color-blue”

@property --color-blue {
  syntax: "<color>";
  inherits: true;
  initial-value: blue;
}

/* Prior rules also apply */

Eu sou dabadee azul (talvez)

Sucesso, nosso texto ainda está azul apesar da definição inválida!

Além disso, o DevTools agora é útil novamente:

O DevTools exibe o valor inválido dentro da regra do parágrafo como riscado e com um ícone de erro, e também fornece uma sobreposição de foco para a propriedade personalizada --color-blue com a definição completa fornecida em @property

Podemos observar que o valor inválido é claramente um erro e também nos é fornecida a definição completa da propriedade personalizada por meio da sobreposição de foco.

Fornecendo tipos via syntax

Por que precisaríamos de tipos para propriedades personalizadas? Aqui estão algumas razões:

  • os tipos ajudam a verificar o que torna um valor válido ou inválido
  • sem tipos, as propriedades personalizadas são muito abertas e podem assumir quase qualquer valor, incluindo um espaço em branco
  • a falta de tipos impede que o DevTools do navegador forneça o nível ideal de detalhes sobre qual valor está em uso para uma propriedade personalizada

Em nossa @propertydefinição, o syntaxdescritor permite fornecer os tipos permitidos para a propriedade personalizada. Usamos "<color>", mas outros tipos incluem:

  • "<length>"– números com unidades anexadas, ex. 4pxou3vw
  • "<integer>"– unidades decimais de 0 a 9 (também conhecidas como “números inteiros”)
  • "<number>"– números que podem ter uma fração, ex.1.25
  • "<percentage>"– números com um sinal de porcentagem anexado, ex.24%
  • "<length-percentage>"– aceita valores válidos <length>ou<percentage>

Um caso especial é "*"que significa “sintaxe universal” e permite aceitar qualquer valor, similar ao comportamento padrão. Isso significa que você pula o benefício da digitação, mas talvez queira a herança e/ou o controle do valor inicial.

Esses tipos e muito mais são listados para o descritor de sintaxe no MDN.

O tipo se aplica ao valor computado da propriedade personalizada, portanto, o "<color>"tipo ficaria satisfeito com ambos, blue bem como light-dark(blue, cyan)(embora apenas um deles seja aceito, initial-value como aprenderemos em breve).

Digitação mais forte com listas

Digamos que queremos fornecer um pouco de flexibilidade para nossa --color-bluepropriedade personalizada.

Podemos usar uma lista para fornecer opções válidas. Qualquer coisa diferente desses valores exatos seria considerada inválida, e use the initial-value em vez disso (se a herança não se aplicasse). Elas são chamadas de “idents personalizados”, diferenciam maiúsculas de minúsculas e podem ser qualquer valor.CSS para “Definindo uma lista dentro da sintaxe”

@property --color-blue {
  syntax: "blue | cyan | dodgerblue";
  inherits: true;
  initial-value: blue;
}

.color-blue {
  color: var(--color-blue);
}

.demo p {
  --color-blue: dodgerblue;
}

Eu sou dabadee azul (talvez)

Digitação para valores mistos

O caractere pipe ( |) usado na lista anterior indica uma condição “ou”. Embora tenhamos usado nomes de cores explícitos, ele também pode ser usado para dizer “qualquer um desses tipos de sintaxe é válido”.

syntax: "<color> | <length>";

Digitando para vários valores

Até agora, digitamos apenas propriedades personalizadas que esperam um único valor.

Dois casos adicionais podem ser cobertos com um caractere “multiplicador” adicional, que deve seguir imediatamente o nome do componente de sintaxe.

  • Use +para suportar uma lista separada por espaços, por exemplo."<length>+"
  • Use #para dar suporte a uma lista separada por vírgulas, por exemplo."<length>#"

Isso pode ser útil para propriedades que permitem múltiplas definições, como background-image.CSS para “Suporte a múltiplos valores para sintaxe”

@property --bg-gradient{
  syntax: "<image>#";
  inherits: false;
  initial-value: 
    repeating-linear-gradient(to right, blue 10px 12px, transparent 12px 22px), 
    repeating-linear-gradient(to bottom, blue 10px 12px, transparent 12px 22px);
}

.box {
  background-image: var(--bg-gradient);

  inline-size: 5rem;
  aspect-ratio: 1;
  border-radius: 4px;
  border: 1px solid;
}

Digitação para valores mistos multipartes

Algumas propriedades aceitam tipos mistos para desenvolver o valor total, como box-shadowwhich tem tipos potenciais de inset, uma série de <length> valores e um <color>.

Atualmente, não é possível digitar isso em uma única @propertydefinição, embora você possa tentar algo como "<length> + <color>". No entanto, isso efetivamente invalida a @propertydefinição em si.

Uma alternativa é dividir as definições de propriedade personalizadas para que possamos permitir uma série de comprimentos e, então, permitir uma cor. Embora seja um pouco mais trabalhoso, isso nos permite ainda obter o benefício da digitação, o que alivia os erros potenciais que abordamos anteriormente.CSS para “Suporte a valores mistos multipartes para sintaxe”

@property --box-shadow-length {
  syntax: "<length>+";
  inherits: false;
  initial-value: 0px 0px 8px 2px;
}

@property --box-shadow-color {
  syntax: "<color>";
  inherits: false;
  initial-value: hsl(0 0% 75%);
}

.box {
  box-shadow: var(--box-shadow-length) var(--box-shadow-color);

  inline-size: 5rem;
  aspect-ratio: 1;
  border-radius: 4px;
}

Permitindo qualquer tipo

Se você estiver menos preocupado com o “tipo” de uma propriedade para algo como box-shadowe se importar mais com herança ou o valor inicial, você pode usar a definição de sintaxe universal para permitir qualquer valor. Isso anula o problema que acabamos de mitigar ao dividir as partes.

@property --box-shadow {
  syntax: "*";
  inherits: false;
  initial-value: 0px 0px 8px 2px hsl(0 0% 75%);
}

Como a sintaxe universal aceita qualquer valor, um “multiplicador” adicional não é necessário.

Observação : initial-value ainda é necessário que seja computacionalmente independente, como aprenderemos em breve sob as limitações do valor inicial.

Assine minha newsletter para receber atualizações de artigos, dicas de CSS e recursos de front-end!E-mailInscrever-se

Modificando Herança

Um subconjunto de propriedades CSS são herdáveis, como color. O inherits descritor para seu @property registro permite que você controle esse comportamento para sua propriedade personalizada.

Se true, o valor computado pode procurar um ancestral para seu valor se a propriedade não estiver explicitamente definida e, se um valor não for encontrado, ele usará o valor inicial.

Se false, o valor calculado usará o valor inicial se a propriedade não for definida explicitamente para o elemento, como por meio de uma regra de classe ou elemento.

Nesta demonstração, o --box-bgfoi definido como inherits: false, e somente a caixa externa tem uma definição explícita por meio da classe aplicada. A caixa interna usa o valor inicial, pois herança não é permitida.CSS para “Resultado da configuração herda: false”

@property --box-bg {
  syntax: "<color>";
  inherits: false;
  initial-value: cyan;
}

.box {
  background-color: var(--box-bg);

  aspect-ratio: 1;
  border-radius: 4px;
  padding: 1.5rem;
}

.outer-box {
  --box-bg: purple;
}

Uso válido de initial-value

A menos que sua sintaxe esteja aberta a qualquer valor usando a definição de sintaxe universal – "*"– então é necessário definir um initial-value para obter os benefícios de registrar uma propriedade personalizada.

Como já experimentamos, o uso de initial-value foi crítico para evitar a condição de uma renderização completamente quebrada devido a invalid at computed value time. Aqui estão alguns outros benefícios de usar @property com um initial-value.

Ao construir sistemas de design ou bibliotecas de UI, é importante garantir que suas propriedades personalizadas sejam robustas e confiáveis. Fornecer um initial-value pode ajudar a evitar uma experiência quebrada. Além disso, digitar propriedades também combina bem com manter a intenção de tokens de design que podem ser expressos como propriedades personalizadas.

Cenários de computação dinâmica, como o uso de clamp()têm o potencial de incluir um valor inválido, seja por um erro ou pelo navegador não suportar algo dentro da função. Ter um fallback via initial-valuegarante que seu design permaneça funcional. Esse comportamento de fallback é uma salvaguarda para recursos não suportados também, embora isso possa ser limitado pelo @property suporte da regra no navegador que está sendo usado.

Revise maneiras adicionais de evitar invalidade no momento do cálculo que podem ser mais apropriadas para a matriz de suporte do seu navegador, especialmente para cenários críticos.

A incorporação @propertycom initial-value não só melhora a confiabilidade do seu CSS, mas também abre a porta para a possibilidade de melhores ferramentas em torno de propriedades personalizadas. Nós visualizamos a mudança de comportamento no DevTools do navegador, mas estou esperançoso por uma expansão de ferramentas, incluindo plugins IDE.

A camada adicional de segurança do uso @propertydo with initial-value ajuda a manter a intenção do seu design, mesmo que ele não seja perfeito para todos os contextos.

Limitações de initial-value

initial-value está sujeito ao que syntax você define para @property. Além disso, syntaxele mesmo não suporta todas as combinações de valores possíveis, que abordamos anteriormente. Então, às vezes, um pouco de criatividade é necessária para obter o benefício.

Além disso, initial-valueos valores devem ser o que a especificação chama de computacionalmente independentes. Simplificando, isso significa que valores relativos como emou funções dinâmicas como clamp()ou light-dark()infelizmente não são permitidos. No entanto, nesses cenários, você ainda pode definir um valor inicial aceitável e, em seguida, usar um valor relativo ou dinâmico ao usar a propriedade personalizada, como na :rootatribuição.

@property --heading-font-size {
  syntax: "<length>";
  inherits: true;
  initial-value: 24px;
}

:root {
  --heading-font-size: clamp(1.25rem, 5cqi, 2rem);
}

Essa limitação em unidades relativas ou funções dinâmicas também significa que outras propriedades personalizadas não podem ser usadas na  initial-value atribuição. A técnica anterior ainda pode ser usada para mitigar isso, onde o resultado preferido é composto no uso da propriedade.

Por fim, propriedades personalizadas registradas via @property ainda estão bloqueadas nas regras de propriedades regulares, como não poderem ser usadas para habilitar variáveis ​​em media ou container query at-rules. Por exemplo, @media (min-width: var(--mq-md))ainda seriam inválidas.

Sem suporte initial-value pode travar a página

No momento em que este artigo foi escrito, usar um valor de propriedade ou função que um navegador pode não suportar como parte da initial-valuedefinição pode fazer com que a página inteira trave!

Felizmente, podemos @supportstestar propriedades ou recursos ultramodernos antes de tentar usá-los como initial-value.

@supports ([property|feature]) {
  /* Feature is supported, use for initial-value */
}

@supports not ([property|feature]) {
  /* Feature unsupported, use alternate for initial-value */
}

Ainda pode haver algumas surpresas onde @supportsos relatórios são verdadeiros, mas o teste revelará uma falha ou outro erro (ex. currentColorusado com color-mix()no Safari). Certifique-se de testar suas soluções em vários navegadores!

Saiba mais sobre maneiras de testar o suporte a recursos para CSS moderno.

Exceções às limitações dinâmicas

Existem algumas condições que podem parecer exceções ao requisito de valores “computacionalmente independentes” quando usados ​​para o initial-value.

Primeiro, currentColor é aceito. Diferentemente de um valor relativo como emque requer computação font-sizede ancestrais para computar a si mesmo, o valor de currentColorpode ser computado sem depender do contexto.CSS para “Uso de currentColor como valor inicial”

@property --border-color {
  syntax: "<color>";
  inherits: false;
  initial-value: currentColor;
}

h2 {
  color: blue;
  border: 3px solid var(--border-color);
}

Minha borda está definida para currentColor

Segundo, o uso de "<length-percentage>"permite o uso de calc(), que é mencionado na especificação. Isso permite um cálculo que inclui o que é considerado um conjunto de unidades global e computacionalmente independente, embora frequentemente os usemos para comportamento dinâmico. Ou seja, o uso de unidades de viewport.

Para um cenário como o do tipo fluido, isso fornece um melhor fallback que mantém o espírito do resultado pretendido, embora seja, no geral, menos ideal para a maioria dos cenários. CSS para “Uso de calc() com vi para valor inicial”

@property --heading-font-size {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: calc(18px + 1.5vi);
}

/* In practice, define your ideal sizing function
using `clamp()` via an assignment on `:root` */

h2 {
  font-size: var(--heading-font-size);
}

Redimensione a janela para ver o comportamento do fluido

Observação : embora normalmente recomendemos usar rempara font-size definições, ele é considerado um valor relativo e não é aceito para uso em initial-value, daí o uso de pxno cálculo.

Consequências da configuração initial-value

Em alguns cenários, registrar uma propriedade sem a sintaxe universal – o que significa que initial-value é necessário – tem consequências e limita o uso da propriedade.

Alguns motivos para preferir propriedades de componentes opcionais incluem:

  • para usar o método de fallback de propriedade personalizada regular para seu valor padrão, especialmente se o fallback deve ser outra propriedade personalizada (por exemplo, um token de design)
  • pode initial-value resultar em uma condição padrão indesejada, principalmente porque não pode incluir outra propriedade personalizada

Uma técnica que adoro usar para estilos de componentes flexíveis é incluir uma propriedade personalizada intencionalmente indefinida para que variantes possam ser criadas eficientemente apenas atualizando o valor da propriedade personalizada. Ou, usar propositalmente propriedades totalmente indefinidas para tornar a classe base mais inclusiva de vários cenários, tratando propriedades personalizadas como uma API de estilo de componente.

Por exemplo, se eu registrasse --button-background aqui como uma cor, ele nunca usaria o fallback correto quando minha intenção era que a variante padrão usasse o fallback.

.button {
  /* Use of initial-value would prevent ever using the fallback */
  background-color: var(--button-background, var(--color-primary));

  /* Intended to be undefined and therefore considered invalid until set */
  border-radius: var(--button-border-radius);
}

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

.button--rounded {
  --button-border-radius: 4px;
}

Se você também tiver esses cenários, pode considerar usar uma abordagem mista de digitação de suas propriedades primitivas – como --color-primary– mas não as propriedades específicas do componente.

Considerações para uso @property

Embora algumas das demonstrações neste artigo tenham sido intencionalmente configuradas para que a saída renderizada use apenas o initial-value, na prática seria melhor definir separadamente a propriedade personalizada. Novamente, este é atualmente um novo recurso, então sem uma definição adicional como em :rootvocê corre o risco de não ter nenhum valor se trocar para confiar apenas em initial-value.

Você também deve estar ciente de que é possível registrar a mesma propriedade várias vezes, e que regras em cascata significam que a última vencerá. Isso aumenta o potencial para conflitos de substituições acidentais. Não há uma maneira de “escopo” da @property regra dentro de um seletor.

No entanto, o uso de camadas em cascata pode modificar esse comportamento, já que estilos sem camadas vencem estilos em camadas, o que inclui at-rules. Camadas em cascata podem ser uma maneira de gerenciar o registro de @propertyregras se você atribuir uma camada de “propriedades” logo no início e se comprometer a atribuir todos os registros a essa camada.

Propriedades personalizadas também podem ser registradas via JavaScript. Na verdade, essa era a maneira original de fazer isso, já que essa capacidade era originalmente acoplada às APIs do Houdini. Se uma propriedade for registrada via JS, essa definição provavelmente vencerá a das suas folhas de estilo. Dito isso, se sua intenção real é alterar um valor de propriedade personalizada via JS, aprenda a maneira mais apropriada de acessar e definir propriedades personalizadas com JS.

O uso de @propertytem o potencial de fortalecer consultas de estilo de contêiner, especialmente se você estiver registrando propriedades para atuar como toggles ou enums. Neste exemplo, o uso de @propertyajuda ao digitar nossos valores de tema e garante um fallback de “light”.

@property --theme {
  syntax: "light | dark";
  inherits: true;
  initial-value: light;
}

:root {
  --theme: dark;
}

@container style(--theme: dark) {
  body {
    background-color: black;
    color: white;
  }
}

Embora esteja um pouco fora do escopo deste artigo, outro benefício de digitar propriedades personalizadas é que elas se tornam animáveis. Isso ocorre porque o tipo transforma o valor em algo com o qual o CSS sabe como trabalhar, em vez do misterioso valor aberto que seria de outra forma. Aqui está um exemplo do CodePen de como registrar uma propriedade personalizada de cor permite animar uma gama de cores para o plano de fundo.


O uso de @propertypermite escrever propriedades CSS personalizadas mais seguras, o que melhora a confiabilidade do design do seu sistema e defende contra erros que podem impactar a experiência do usuário. Um lembrete de que, por enquanto, elas são um aprimoramento progressivo e devem quase sempre ser usadas em conjunto com uma definição explícita da propriedade.

Certifique-se de testar para garantir o resultado pretendido tanto da sintaxe permitida quanto do resultado se ela initial-valuefor usada na renderização final.

Postado em BlogTags:
Escreva um comentário