Propriedades personalizadas de CSS na cascata
Publicados: 2022-03-10No mês passado, tive uma conversa no Twitter sobre a diferença entre estilos “com escopo” (gerados em um processo de construção) e estilos “aninhados” nativos do CSS. Perguntei por que, curiosamente, os desenvolvedores evitam a especificidade dos seletores de ID, enquanto adotam “estilos com escopo” gerados pelo JavaScript? Keith Grant sugeriu que a diferença está em equilibrar a cascata* e a herança, ou seja, dar preferência à proximidade sobre a especificidade. Vamos dar uma olhada.
A Cascata
A cascata CSS é baseada em três fatores:
- Importância definida pelo sinalizador
!important
e origem do estilo (usuário > autor > navegador) - Especificidade dos seletores usados (inline > ID > class > element)
- Ordem de origem do próprio código (o mais recente tem precedência)
A proximidade não é mencionada em nenhum lugar — o relacionamento DOM-árvore entre partes de um seletor. Os parágrafos abaixo serão ambos vermelhos, embora #inner p
descreva uma relação mais próxima do que #outer p
para o segundo parágrafo:
<section> <p>This text is red</p> <div> <p>This text is also red!</p> </div> </section>
#inner p { color: green; } #outer p { color: red; }
Ambos os seletores têm a mesma especificidade, ambos descrevem o mesmo elemento p
e nenhum deles é sinalizado como !important
— portanto, o resultado é baseado apenas na ordem de origem.
BEM e estilos de escopo
Convenções de nomenclatura como BEM (“Block__Element—Modifier”) são usadas para garantir que cada parágrafo tenha “escopo” para apenas um pai, evitando totalmente a cascata. Os “elementos” de parágrafo recebem classes exclusivas específicas para seu contexto de “bloco”:
<section class="outer"> <p class="outer__p">This text is red</p> <div class="inner"> <p class="inner__p">This text is green!</p> </div> </section>
.inner__p { color: green; } .outer__p { color: red; }
Esses seletores ainda têm a mesma importância relativa, especificidade e ordem de origem, mas os resultados são diferentes. Ferramentas CSS “com escopo” ou “modular” automatizam esse processo, reescrevendo nosso CSS para nós, com base no HTML. No código abaixo, cada parágrafo tem como escopo seu pai direto:
<section outer-scope> <p outer-scope>This text is red</p> <div outer-scope inner-scope> <p inner-scope>This text is green!</p> </div> </section>
p[inner-scope] { color: green } p[outer-scope] { color: red; }
Herança
A proximidade não faz parte da cascata, mas faz parte do CSS. É aí que a herança se torna importante. Se retirarmos o p
de nossos seletores, cada parágrafo herdará uma cor de seu ancestral mais próximo:
#inner { color: green; } #outer { color: red; }
Como #inner
e #outer
descrevem elementos diferentes, nosso div
e section
respectivamente, ambas as propriedades de cor são aplicadas sem conflito. O elemento p
aninhado não tem cor especificada, portanto, os resultados são determinados por herança (a cor do pai direto) em vez de cascata . A proximidade tem precedência e o valor #inner
substitui o #outer
.
Mas há um problema: para usar herança, estamos estilizando tudo dentro de nossa section
e div
. Queremos segmentar especificamente a cor do parágrafo.
(Re-)Apresentando Propriedades Personalizadas
As propriedades personalizadas fornecem uma nova solução nativa do navegador; eles herdam como qualquer outra propriedade, mas não precisam ser usados onde são definidos . Usando CSS simples, sem convenções de nomenclatura ou ferramentas de construção, podemos criar um estilo direcionado e contextual, com a proximidade tendo precedência sobre a cascata:
p { color: var(--paragraph); } #inner { --paragraph: green; } #outer { --paragraph: red; }
A propriedade --paragraph
customizada herda exatamente como a propriedade color
, mas agora temos controle sobre exatamente como e onde esse valor é aplicado. A propriedade --paragraph
atua de forma semelhante a um parâmetro que pode ser passado para o componente p
, seja por seleção direta (regras de especificidade) ou contexto (regras de proximidade).
Acho que isso revela um potencial para propriedades personalizadas que geralmente associamos a funções, mixins ou componentes.
“Funções” e parâmetros personalizados
Funções, mixins e componentes são todos baseados na mesma ideia: código reutilizável, que pode ser executado com vários parâmetros de entrada para obter resultados consistentes, mas configuráveis. A distinção está no que eles fazem com os resultados. Começaremos com uma variável de gradiente listrado e, em seguida, podemos estendê-la para outras formas:
html { --stripes: linear-gradient( to right, powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Essa variável é definida no elemento html
raiz (também pode usar :root
, mas isso adiciona especificidade desnecessária), então nossa variável listrada estará disponível em todo o documento. Podemos aplicá-lo em qualquer lugar que os gradientes sejam suportados:
body { background-image: var(--stripes); }
Adicionando Parâmetros
As funções são usadas como variáveis, mas definem parâmetros para alterar a saída. Podemos atualizar nossa variável --stripes
para ser mais parecida com uma função, definindo algumas variáveis semelhantes a parâmetros dentro dela. Vou começar substituindo to right
por var(--stripes-angle)
, para criar um parâmetro de mudança de ângulo:
html { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
Existem outros parâmetros que podemos criar, dependendo de qual finalidade a função deve servir. Devemos permitir que os usuários escolham suas próprias cores de listras? Em caso afirmativo, nossa função aceita 5 parâmetros de cores diferentes ou apenas 3 que ficarão de fora para dentro como temos agora? Queremos criar parâmetros para color-stops também? Cada parâmetro que adicionamos fornece mais personalização ao custo de simplicidade e consistência.
Não há uma resposta universal certa para esse equilíbrio – algumas funções precisam ser mais flexíveis e outras precisam ser mais opinativas. As abstrações existem para fornecer consistência e legibilidade em seu código, então dê um passo atrás e pergunte quais são seus objetivos. O que realmente precisa ser personalizável e onde a consistência deve ser aplicada? Em alguns casos, pode ser mais útil ter duas funções opinativas, em vez de uma função totalmente personalizável.
Para usar a função acima, precisamos passar um valor para o parâmetro --stripes-angle
e aplicar a saída a uma propriedade de saída CSS, como background-image
:
/* in addition to the code above… */ html { --stripes-angle: 75deg; background-image: var(--stripes); }
Herdado versus Universal
Eu defini a função --stripes
no elemento html
por hábito. Propriedades personalizadas são herdadas e eu quero minha função disponível em todos os lugares, então faz algum sentido colocá-la no elemento raiz. Isso funciona bem para herdar variáveis como --brand-color: blue
, então também podemos esperar que funcione para nossa “função”. Mas se tentarmos usar esta função novamente em um seletor aninhado, não funcionará:
div { --stripes-angle: 90deg; background-image: var(--stripes); }
O novo --stripes-angle
é ignorado completamente. Acontece que não podemos confiar na herança para funções que precisam ser recalculadas. Isso porque cada valor de propriedade é calculado uma vez por elemento (no nosso caso, o elemento raiz html
) e, em seguida, o valor calculado é herdado . Ao definir nossa função na raiz do documento, não disponibilizamos a função inteira para os descendentes — apenas o resultado calculado de nossa função.
Isso faz sentido se você enquadrá-lo em termos do parâmetro --stripes-angle
em cascata. Como qualquer propriedade CSS herdada, está disponível para descendentes, mas não para ancestrais. O valor que definimos em uma div
aninhada não está disponível para uma função que definimos no ancestral raiz html
. Para criar uma função universalmente disponível que irá recalcular em qualquer elemento, temos que defini-la em cada elemento:
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
O seletor universal disponibiliza nossa função em todos os lugares, mas podemos defini-la de forma mais restrita, se quisermos. O importante é que ele só pode recalcular onde está explicitamente definido. Aqui estão algumas alternativas:
/* make the function available to elements with a given selector */ .stripes { --stripes: /* etc… */; } /* make the function available to elements nested inside a given selector */ .stripes * { --stripes: /* etc… */; } /* make the function available to siblings following a given selector */ .stripes ~ * { --stripes: /* etc… */; }
Isso pode ser estendido com qualquer lógica de seletor que não dependa de herança.
Parâmetros livres e valores de fallback
Em nosso exemplo acima, var(--stripes-angle)
não tem valor nem fallback. Ao contrário das variáveis Sass ou JS que devem ser definidas ou instanciadas antes de serem chamadas, as propriedades customizadas CSS podem ser chamadas sem nunca serem definidas. Isso cria uma variável “livre”, semelhante a um parâmetro de função que pode ser herdado do contexto.
Podemos eventualmente definir a variável em html
ou :root
(ou qualquer outro ancestral) para definir um valor herdado, mas primeiro precisamos considerar o fallback se nenhum valor for definido. Existem várias opções, dependendo exatamente do comportamento que queremos
- Para parâmetros “obrigatórios”, não queremos um fallback. Como está, a função não fará nada até que
--stripes-angle
seja definido. - Para parâmetros “opcionais”, podemos fornecer um valor de fallback na função
var()
. Após o nome da variável, adicionamos uma vírgula, seguida do valor padrão:
var(--stripes-angle, 90deg)
Cada função var()
só pode ter um fallback — portanto, qualquer vírgula adicional fará parte desse valor. Isso torna possível fornecer padrões complexos com vírgulas internas:
html { /* Computed: Hevetica, Ariel, sans-serif */ font-family: var(--sans-family, Hevetica, Ariel, sans-serif); /* Computed: 0 -1px 0 white, 0 1px 0 black */ test-shadow: var(--shadow, 0 -1px 0 white, 0 1px 0 black); }
Também podemos usar variáveis aninhadas para criar nossas próprias regras de cascata, dando diferentes prioridades aos diferentes valores:
var(--stripes-angle, var(--global-default-angle, 90deg))
- Primeiro, tente nosso parâmetro explícito (
--stripes-angle
); - Fallback para um “padrão do usuário” global (
--user-default-angle
) se estiver disponível; - Finalmente, retorne ao nosso “padrão de fábrica”
(90deg
).
Ao definir valores de fallback em var()
em vez de definir a propriedade personalizada explicitamente, garantimos que não haja restrições de especificidade ou cascata no parâmetro. Todos os parâmetros *-angle
são “livres” para serem herdados de qualquer contexto.
Fallbacks do navegador versus fallbacks variáveis
Quando estamos usando variáveis, há dois caminhos de fallback que precisamos ter em mente:
- Qual valor deve ser usado por navegadores sem suporte a variáveis?
- Qual valor deve ser usado por navegadores que suportam variáveis, quando uma determinada variável está ausente ou é inválida?
p { color: blue; color: var(--paragraph); }
Enquanto os navegadores antigos ignorarão a propriedade de declaração de variável e retornarão para blue
- os navegadores modernos lerão ambos e usarão o último. Nosso var(--paragraph)
pode não estar definido, mas é válido e substituirá a propriedade anterior, portanto, os navegadores com suporte a variáveis retornarão ao valor herdado ou inicial, como se estivessem usando a palavra-chave unset
.
Isso pode parecer confuso no começo, mas há boas razões para isso. A primeira é técnica: os mecanismos do navegador lidam com sintaxe inválida ou desconhecida no “tempo de análise” (o que acontece primeiro), mas as variáveis não são resolvidas até o “tempo do valor calculado” (o que acontece mais tarde).
- No momento da análise, as declarações com sintaxe inválida são ignoradas completamente — voltando-se para as declarações anteriores. Este é o caminho que os navegadores antigos seguirão. Os navegadores modernos suportam a sintaxe de variável, portanto, a declaração anterior é descartada.
- No momento do valor calculado a variável é compilada como inválida, mas é tarde demais — a declaração anterior já foi descartada. De acordo com a especificação, valores de variáveis inválidos são tratados da mesma forma que
unset
:
html { color: red; /* ignored as *invalid syntax* by all browsers */ /* - old browsers: red */ /* - new browsers: red */ color: not a valid color; color: var(not a valid variable name); /* ignored as *invalid syntax* by browsers without var support */ /* valid syntax, but invalid *values* in modern browsers */ /* - old browsers: red */ /* - new browsers: unset (black) */ --invalid-value: not a valid color value; color: var(--undefined-variable); color: var(--invalid-value); }
Isso também é bom para nós como autores, porque nos permite jogar com fallbacks mais complexos para os navegadores que suportam variáveis e fornecer fallbacks simples para navegadores mais antigos. Melhor ainda, isso nos permite usar o estado null
/ undefined
para definir os parâmetros necessários. Isso se torna especialmente importante se quisermos transformar uma função em um mixin ou componente.
Propriedade personalizada "Mixins"
No Sass, as funções retornam valores brutos, enquanto os mixins geralmente retornam a saída CSS real com pares propriedade-valor. Quando definimos uma propriedade --stripes
universal, sem aplicá-la a nenhuma saída visual, o resultado é semelhante a uma função. Podemos fazer isso se comportar mais como um mixin, definindo a saída universalmente também:
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Enquanto --stripes-angle
permanecer inválido ou indefinido, o mixin falhará ao compilar e nenhuma background-image
será aplicada. Se definirmos um ângulo válido em qualquer elemento, a função calculará e nos dará um plano de fundo:
div { --stripes-angle: 30deg; /* generates the background */ }
Infelizmente, esse valor de parâmetro será herdado, portanto, a definição atual cria um plano de fundo no div
e em todos os descendentes . Para corrigir isso, temos que garantir que o valor --stripes-angle
não seja herdado, colocando-o como initial
(ou qualquer valor inválido) em cada elemento. Podemos fazer isso no mesmo seletor universal:
* { --stripes-angle: initial; --stripes: /* etc… */; background-image: var(--stripes); }
Estilos Inline Seguros
Em alguns casos, precisamos que o parâmetro seja definido dinamicamente a partir de CSS externo — com base em dados de um servidor de back-end ou estrutura de front-end. Com propriedades personalizadas, podemos definir variáveis em nosso HTML com segurança sem nos preocuparmos com os problemas de especificidade usuais:
<div>...</div>
Os estilos embutidos têm uma alta especificidade e são muito difíceis de substituir - mas com propriedades personalizadas, temos outra opção: ignorá-lo. Se definirmos o div para background-image: none
(por exemplo), essa variável inline não terá impacto. Para ir ainda mais longe, podemos criar uma variável intermediária:
* { --stripes-angle: var(--stripes-angle-dynamic, initial); }
Agora temos a opção de definir --stripes-angle-dynamic
no HTML, ou ignorá-lo, e definir --stripes-angle
diretamente em nossa folha de estilo.
Valores predefinidos
Para valores mais complexos ou padrões comuns que queremos reutilizar, também podemos fornecer algumas variáveis predefinidas para escolher:
* { --tilt-down: 6deg; --tilt-up: -6deg; }
E use essas predefinições, em vez de definir o valor diretamente:
<div>...</div>
Isso é ótimo para criar tabelas e gráficos com base em dados dinâmicos, ou até mesmo criar um planejador diário.
Componentes contextuais
Também podemos reformular nosso “mixin” como um “componente” aplicando-o a um seletor explícito e tornando os parâmetros opcionais. Em vez de contar com a presença ou ausência de --stripes-angle
para alternar nossa saída, podemos contar com a presença ou ausência de um seletor de componentes. Isso nos permite definir valores de fallback com segurança:
[data-stripes] { --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Ao colocar o fallback dentro da função var()
, podemos deixar --stripes-angle
indefinido e “livre” para herdar um valor de fora do componente. Essa é uma ótima maneira de expor certos aspectos de um estilo de componente à entrada contextual. Mesmo estilos “com escopo” gerados por uma estrutura JS (ou com escopo dentro do shadow-DOM, como ícones SVG) podem usar essa abordagem para expor parâmetros específicos para influência externa.
Componentes isolados
Se não quisermos expor o parâmetro para herança, podemos definir a variável com um valor padrão:
[data-stripes] { --stripes-angle: to right; --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
Esses componentes também funcionariam com uma classe ou qualquer outro seletor válido, mas escolhi o atributo data-
para criar um namespace para qualquer modificador que queiramos:
[data-stripes='vertical'] { --stripes-angle: to bottom; } [data-stripes='horizontal'] { --stripes-angle: to right; } [data-stripes='corners'] { --stripes-angle: to bottom right; }
Seletores e Parâmetros
Muitas vezes, desejo poder usar atributos de dados para definir uma variável — um recurso suportado pela especificação CSS3 attr()
, mas ainda não implementado em nenhum navegador (consulte a guia de recursos para problemas vinculados em cada navegador). Isso nos permitiria associar mais de perto um seletor a um parâmetro específico:
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
Enquanto isso, podemos conseguir algo semelhante usando o atributo style
:
<div>...</div> /* The `*=` atttribute selector will match a string anywhere in the attribute */ [style*='--stripes-angle'] { /* Only define the function where we want to call it */ --stripes: linear-gradient(…); }
Essa abordagem é mais útil quando queremos incluir outras propriedades além do parâmetro que está sendo definido. Por exemplo, definir uma área de grade também pode adicionar preenchimento e plano de fundo:
[style*='--grid-area'] { background-color: white; grid-area: var(--grid-area, auto / 1 / auto / -1); padding: 1em; }
Conclusão
Quando começamos a juntar todas essas peças, fica claro que as propriedades personalizadas vão muito além dos casos de uso de variáveis comuns com os quais estamos familiarizados. Não apenas podemos armazenar valores e defini-los para a cascata — mas podemos usá-los para manipular a cascata de novas maneiras e criar componentes mais inteligentes diretamente no CSS.
Isso nos obriga a repensar muitas das ferramentas nas quais confiamos no passado – desde convenções de nomenclatura como SMACSS e BEM, até estilos “com escopo” e CSS-in-JS. Muitas dessas ferramentas ajudam a contornar a especificidade ou a gerenciar estilos dinâmicos em outra linguagem — casos de uso que agora podemos abordar diretamente com propriedades personalizadas. Estilos dinâmicos que geralmente calculamos em JS agora podem ser manipulados passando dados brutos para o CSS.
A princípio, essas mudanças podem ser vistas como “complexidade adicional” — já que não estamos acostumados a ver lógica dentro do CSS. E, como em todo código, o excesso de engenharia pode ser um perigo real. Mas eu diria que, em muitos casos, podemos usar esse poder não para adicionar complexidade, mas para remover a complexidade de ferramentas e convenções de terceiros, de volta à linguagem central do design da web e (mais importante) de volta ao navegador. Se nossos estilos exigem cálculo, esse cálculo deve ficar dentro de nosso CSS.
Todas essas ideias podem ser levadas muito mais longe. As propriedades personalizadas estão apenas começando a ter uma adoção mais ampla e apenas começamos a arranhar a superfície do que é possível. Estou animado para ver onde isso vai dar, e o que mais as pessoas vão inventar. Divirta-se!
Leitura adicional
- “É hora de começar a usar propriedades personalizadas CSS”, Serg Hospodarets
- “Um guia de estratégia para propriedades personalizadas de CSS”, Michael Riethmuller