CSS

27 abr, 2016

Por que eu estou animado sobre as variáveis nativas no CSS

Publicidade

Meses atrás, as variáveis CSS – mais conhecidas como propriedades personalizadas do CSS (CSS Custom Properties) – chegaram ao Chrome Canary por meio da Experimental Web Platform (1).

Quando o engenheiro do Chrome Addy Osmani tuitou pela primeira vez sobre o lançamento, ele foi recebido com uma surpreendente quantidade de negatividade, hostilidade e ceticismo. Isso foi surpreendente, pelo menos para mim, dado que estou animado sobre esse recurso.

Depois de um rápido exame das respostas, ficou claro que 99% das reclamações incidiam sobre estas duas coisas:

  • A sintaxe é “feia” e “verbosa”.
  • Sass já tem variáveis, então por que eu deveria me importar?

Apesar de eu admitir que a sintaxe não me agrada, é importante entender que isso não foi escolhido arbitrariamente. Os membros do grupo de trabalho CSS discutiram a sintaxe por muito tempo, e eles tiveram que escolher algo que era compatível com a gramática do CSS e que não entraria em conflito com futuras adições à linguagem.

No que diz respeito as variáveis ​​CSS vs. variáveis ​​Sass, aqui é onde eu acho que o maior equívoco reside:

Variáveis ​​CSS nativas não foram apenas uma tentativa de copiar o que os pré-processamentos CSS já podiam fazer. Na verdade, se você ler algumas das discussões iniciais do projeto, verá que a maior parte da motivação para as variáveis ​​CSS nativas foi tornar possível fazer as coisas que você não pode fazer com o pré-processamento!

Pré-processamentos CSS são ferramentas fantásticas, mas as suas variáveis ​​são estáticas e o escopo é léxico. Variáveis ​​CSS nativas, por outro lado, são um tipo totalmente diferente de variável: elas são dinâmicas, e elas estão com escopo no DOM. Na verdade, eu acho que é confuso chamá-las de variáveis. Elas são realmente propriedades CSS, o que lhes confere um conjunto completamente diferente de capacidades e lhes permite resolver um conjunto completamente diferente de problemas.

Neste artigo, eu vou discutir algumas das coisas que você pode fazer com propriedades personalizadas CSS que você não pode fazer com variáveis ​​pré-processadas. Eu também vou demonstrar alguns dos novos design patterns que as propriedades personalizadas permitem. Finalmente, vou discutir por que eu acho que, no futuro, provavelmente iremos usar variáveis de pré-processamento e propriedades personalizadas em conjunto, para aproveitar o melhor dos dois mundos.

Nota: este artigo não é uma introdução para propriedades personalizadas CSS. Se você nunca ouviu falar delas ou não está familiarizado com a forma como elas funcionam, eu recomendo tentar familiarizar-se primeiro.

As limitações das variáveis ​​de pré-processamento

Antes de continuar, quero salientar que eu realmente gosto do pré-processamento CSS, e eu o uso em todos os meus projetos. Pré-processadores podem fazer algumas coisas bem surpreendentes, e mesmo se você sabe que em última análise eles apenas cospem CSS puro, eles ainda podem parecer mágicos às vezes.

Dito isso, como qualquer ferramenta, eles têm as suas limitações, e às vezes a aparência de poder dinâmico pode fazer essas limitações surpreendentes, especialmente para novos usuários.

Variáveis ​​de pré-processamento não são em tempo real

Talvez o exemplo mais comum de uma limitação do pré-processamento que surpreende os recém-chegados é a incapacidade do Sass para definir variáveis ​​ou usar @extend dentro de uma media query. Como este artigo é sobre variáveis, vou me concentrar nisso:

$gutter: 1em;

@media (min-width: 30em) {
  $gutter: 2em;
}

.Container {
  padding: $gutter;
}

Se você compilar o código acima, isto é o que você terá:

.Container {
  padding: 1em;
}

Como você pode ver, o bloco de media query simplesmente é descartado e a atribuição de variável, ignorada.

Embora possa ser teoricamente possível para Sass fazer declarações de variáveis condicionais funcionarem, fazer isso seria um desafio e exigiria enumerar todas as permutações, aumentando exponencialmente o tamanho final do seu CSS.

Como você não pode mudar uma variável com base na regra da @media correspondente, sua única opção é atribuir uma única variável por media query, e um código para cada variação separadamente. Falaremos mais sobre isso mais tarde.

Variáveis de ​​pré-processamento não cascata

Sempre que você usar variáveis, a questão do escopo inevitavelmente entra em jogo. Essa variável deve ser global? Deve ser delimitada para o arquivo/módulo? Deve ser delimitada para o bloco?

Já que o CSS está fazendo o estilo do HTML, há outra maneira útil de ter variáveis ​​de escopo: fazendo um elemento DOM. Mas como pré-processamentos não executam no navegador e nunca verão a marcação, eles não podem fazer isso.

Considere um site que tenta adicionar a classe user-setting-large-text ao elemento <html> para usuários que indicam essa preferência por textos de tamanho grande. Quando essa classe é definida, a atribuição da variável $font-size deve aplicar:

$font-size: 1em;

.user-setting-large-text {
  $font-size: 1.5em;
}

body {
  font-size: $font-size;
}

Mas, novamente, assim como com o exemplo de bloco de mídia acima, Sass ignora essa atribuição de variável por completo, o que significa que esse tipo de coisa não é possível. Aqui está a saída:

body {
  font-size: 1em;
}

Variáveis de ​​pré-processamento não herdam

Embora a herança seja tecnicamente parte da cascata, eu quero chamá-la separadamente por causa de quantas vezes eu já quis usar esse recurso, mas não pude.

Considere uma situação em que você tem elementos DOM que você deseja que usem o estilo com base em qualquer cor que será aplicada aos seus pais:

.alert { background-color: lightyellow; }
.alert.info { background-color: lightblue; }
.alert.error { background-color: orangered; }

.alert button {
  border-color: darken(background-color, 25%);
}

O código acima não é um Sass válido (ou CSS), mas você deve ser capaz de entender o que ele está tentando realizar.

A última declaração está tentando usar a função darken do Sass na propriedade background-color que o elemento <button> pode herdar de seu elemento pai .alert. Se as classes info ou error foram adicionadas ao alerta (ou se background-color foi definido arbitrariamente via JavaScript ou um estilo do usuário), o elemento button quer ser capaz de responder a isso.

Agora, obviamente, isso não vai funcionar no Sass porque pré-processamento não sabe sobre a estrutura DOM, mas espero que esteja claro por que esse tipo de coisa poderia ser útil.

Para chamar um caso de uso especial: seria extremamente útil ser capaz de executar as funções de cor em propriedades herdadas do DOM, por razões de acessibilidade. Por exemplo, para garantir que o texto é sempre suficientemente legível e contrasta com a cor do fundo. Com as propriedades personalizadas e as novas funções de cor CSS, isso em breve será possível!

Variáveis ​de ​pré-processamento não são interoperáveis

Esta é uma desvantagem relativamente óbvia do pré-processamento, mas eu mencionei, porque eu acho que é importante. Se você está construindo um site com PostCSS e quiser usar um componente de terceiros que só é personalizável por meio de Sass, você está sem sorte.

Não é possível (ou, pelo menos, não é fácil) compartilhar variáveis ​​de pré-processamento em diferentes conjuntos de ferramentas ou com folhas de estilo de terceiros hospedadas em um CDN.

Propriedades nativas personalizadas de CSS irão funcionar com qualquer pré-processamento CSS ou arquivo CSS simples. O inverso não é geralmente verdadeiro.

Como propriedades personalizadas são diferentes

Como você provavelmente já adivinhou, nenhuma das limitações que listei acima se aplica às propriedades personalizadas CSS. Mas talvez o mais importante não é que elas não se aplicam, mas o porquê disso.

Propriedades personalizadas CSS são como propriedades CSS normais, e elas funcionam exatamente da mesma forma (com a exceção óbvia de que elas não fazem estilo nenhum).

Assim como propriedades normais CSS, as propriedades personalizadas são dinâmicas. Elas podem ser modificadas em tempo de execução, podem ser atualizadas dentro de uma media query ou pela adição de uma nova classe ao DOM. Elas podem ser atribuídas em linha (ou em um elemento) ou numa declaração CSS normal, com um selector. Elas podem ser atualizadas ou substituídas usando todas as regras normais de cascata ou usando JavaScript. E, talvez o mais importante, elas são herdadas, por isso, quando elas são aplicadas a um elemento DOM, elas são passadas ​​para as filhas do elemento.

Para colocar isso de forma mais sucinta, as variáveis ​​pré-processadas possuem escopo léxico e estático após a compilação. As propriedades personalizadas são escopo para o DOM. Elas estão vivas, e elas são dinâmicas.

Exemplos da vida real

Se você ainda não tem certeza do que as propriedades personalizadas podem fazer que as variáveis ​​pré-processadas não podem, eu tenho alguns exemplos para você.

Dos que valem a pena, existe uma tonelada de grandes exemplos que eu queria mostrar, mas no interesse de não deixar este artigo muito longo, eu separei dois.

Eu escolhi estes exemplos porque não são apenas teoria, eles são desafios reais que enfrentei no passado. Eu lembro perfeitamente quando tentei fazê-los funcionar com pré-processadores, mas simplesmente era impossível. Com propriedades personalizadas, agora é possível.

Propriedades responsivas com media queries

Muitos sites usam uma variável “gap” ou “gutter” que define o espaçamento padrão entre os itens no layout, bem como o preenchimento padrão para todas as várias seções na página. Na maioria das vezes, você quer que o valor de gutter seja diferente, dependendo do tamanho da janela do navegador. Em telas grandes, você quer muito espaço entre os itens (muito espaço para respirar), mas em telas menores você não tem esse luxo, por isso gutter precisa ser menor.

Como eu mencionei acima, as variáveis ​​Sass não funcionam em media queries, então você tem que codificar cada variação separadamente.

O exemplo a seguir define as variáveis ​​$gutterSm, $gutterMd e $gutterLg, e então declara regras separadas para cada variação:

/* Declares three gutter values, one for each breakpoint */

$gutterSm: 1em;
$gutterMd: 2em;
$gutterLg: 3em;

/* Base styles for small screens, using $gutterSm. */

.Container {
  margin: 0 auto;
  max-width: 60em;
  padding: $gutterSm;
}
.Grid {
  display: flex;
  margin: -$gutterSm 0 0 -$gutterSm;
}
.Grid-cell {
  flex: 1;
  padding: $gutterSm 0 0 $gutterSm;
}

/* Override styles for medium screens, using $gutterMd. */

@media (min-width: 30em) {
  .Container {
    padding: $gutterMd;
  }
  .Grid {
    margin: -$gutterMd 0 0 -$gutterMd;
  }
  .Grid-cell {
    padding: $gutterMd 0 0 $gutterMd;
  }
}

/* Override styles for large screens, using $gutterLg. */

@media (min-width: 48em) {
  .Container {
    padding: $gutterLg;
  }
  .Grid {
    margin: -$gutterLg 0 0 -$gutterLg;
  }
  .Grid-cell {
    padding: $gutterLg 0 0 $gutterLg;
  }
}

Para realizar exatamente a mesma coisa usando propriedades personalizadas, você só tem que definir os estilos uma vez. Você pode usar uma única propriedade –gutter, e então, com as mudanças de mídia correspondentes, você atualiza o valor de –gutter e tudo responde de acordo.

/* Declares what `--gutter` is at each breakpoint */

:root { --gutter: 1.5em; }

@media (min-width: 30em) {
  :root { --gutter: 2em; }
}
@media (min-width: 48em) {
  :root { --gutter: 3em; }
}

/*
 * Styles only need to be defined once because
 * the custom property values automatically update.
 */

.Container {
  margin: 0 auto;
  max-width: 60em;
  padding: var(--gutter);
}
.Grid {
  --gutterNegative: calc(-1 * var(--gutter));
  display: flex;
  margin-left: var(--gutterNegative);
  margin-top: var(--gutterNegative);
}
.Grid-cell {
  flex: 1;
  margin-left: var(--gutter);
  margin-top: var(--gutter);
}

Mesmo com a verbosidade extra da sintaxe da propriedade personalizada, a quantidade de código necessária para realizar a mesma coisa é substancialmente reduzida. E isso só leva em conta três variações. Quanto mais variações você tem, mais código isso vai economizar.

A demo a seguir utiliza o código acima para construir um layout de site básico que redefine automaticamente o valor de gutter de acordo com a mudança da largura da viewport. Confira em um navegador que suporta propriedades personalizadas para ver o código em ação!

custom-properties-responsive

Veja a demonstração no CodePen: visualização/página inteira

Estilo contextual

Contextual styling (estilizar um elemento com base em onde ele aparece no DOM) é um tema controverso em CSS. Por um lado, é algo que a maioria dos desenvolvedores respeitados de CSS é contra. Mas, por outro, é algo que a maioria das pessoas ainda faz todos os dias.

Harry Roberts escreveu este artigo com seus pensamentos sobre o assunto:

Se você precisa mudar a aparência de um componente UI com base em onde ele é colocado, o seu sistema de design está errado… As coisas devem ser projetadas para ser ignorantes; as coisas devem ser concebidas de modo que nós sempre temos apenas “esse componente” e não “esse componente quando dentro”…

Mesmo eu estando do lado do Harry em relação a isso (e na maioria das coisas), acho que o fato de que tantas pessoas tomarem atalhos nessas situações seja talvez o indicativo de um problema maior: de que CSS é limitado em sua expressividade, e a maioria das pessoas não está satisfeita com qualquer uma das atuais “melhores práticas”.

O exemplo a seguir mostra como a maioria das pessoas aborda o contextual styling em CSS, usando a combinação descendente:

/* Regular button styles. */
.Button { }

/* Button styles that are different when inside the header. */
.Header .Button { }

Essa abordagem tem uma série de problemas. Uma maneira de reconhecer esse padrão como código ruim é que ele viola o princípio de desenvolvimento de software aberto/fechado; ele modifica os detalhes da implementação de um componente fechado.

Entidades de software (classes, módulos, funções etc.) devem ser abertas para extensão, mas fechadas para modificação.

As propriedades personalizadas mudam o paradigma de definir os componentes de uma maneira interessante. Com propriedades personalizadas, podemos, pela primeira vez, escrever componentes que são realmente abertos para a extensão. Aqui está um exemplo:

.Button {
  background: var(--Button-backgroundColor, #eee);
  border: 1px solid var(--Button-borderColor, #333);
  color: var(--Button-color, #333);
  /* ... */
}

.Header {
  --Button-backgroundColor: purple;
  --Button-borderColor: transparent;
  --Button-color: white;
}

A diferença entre esse e o exemplo da combinação descendente é sutil, mas importante.

Ao usar combinação descendente, nós estamos declarando que os buttons dentro do cabeçalho terão essa aparência, e isso é diferente de como o componente button se define. Tal declaração é ditatorial (pegando emprestado as palavras do Harry) e difícil de desfazer no caso de uma exceção, em que um button no cabeçalho não precisa ter essa aparência.

Com propriedades personalizadas, por outro lado, o componente button ainda é ignorante do seu contexto e completamente dissociado do componente de cabeçalho. Sua declaração simplesmente diz: eu vou me estilizar tendo como base as propriedades personalizadas, o que quer que aconteça na minha situação atual. E o componente de cabeçalho simplesmente diz: Eu vou definir esses valores de propriedade; cabe a meus descendentes determinar se e como utilizá-los.

A principal diferença é que a extensão é opt-in pelo componente button, o que é facilmente desfeito no caso de uma exceção.

A demonstração a seguir ilustra o contextual styling de ambos links e buttons no cabeçalho de um site, bem como na área de conteúdo.

custom-properties-contextual-styling

Veja a demonstração no CodePen: visualização/página inteira

Fazendo exceções

Ilustrar como fazer exceções é mais fácil neste paradigma. Imagine se o componente .Promo fosse adicionado ao cabeçalho e buttons dentro do componente .Primo precisassem parecer um button normal, não button de cabeçalho.

Se você estivesse usando combinação descendente, teria que escrever um monte de estilos para os buttons de cabeçalho e, em seguida, desfazer esses estilos para os buttons do promo; isso é confuso e propenso a erros, e com facilidade perde-se o controle quando o número de combinações aumenta:

/* Regular button styles. */
.Button { }

/* Button styles that are different when inside the header. */
.Header .Button { }

/* Undo button styles in the header that are also in promo. */
.Header .Promo .Button { }

Com propriedades personalizadas, você pode simplesmente atualizar as propriedades do button para ser o que você quiser, ou resetá-las para voltar ao estilo padrão. E, independentemente do número de exceções, a maneira de alterar os estilos é sempre a mesma.

.Promo {
  --Button-backgroundColor: initial;
  --Button-borderColor: initial;
  --Button-color: initial;
}

Aprendendo com o React

Quando eu estava explorando pela primeira vez a ideia do contextual styling por meio de propriedades personalizadas, eu era cético. Como eu disse, a minha inclinação é preferir componentes com contexto agnóstico que definem as suas próprias variações, em vez de se adaptar a dados arbitrários herdados de seu pai.

Mas uma coisa que ajudou a influenciar minha opinião foi comparar as propriedades personalizadas em CSS com props no React.

props no React também são dinâmicos, variáveis de escopo DOM, e são hereditários, o que permite que os componentes sejam dependentes do contexto. No React, componentes-pai passam dados para componentes-filhos, e em seguida componentes-filhos definem que props estão dispostos a aceitar e como eles irão usá-los. Esse modelo de arquitetura é comumente conhecido como fluxo de dados unidirecional.

Mesmo que as propriedades personalizadas sejam algo novo, de domínio não testado, acho que o sucesso do modelo React me dá confiança de que um sistema complexo pode ser construído em cima da propriedade de herança e que as variáveis de escopo ​​DOM são um design pattern útil.

Minimizando os efeitos colaterais

Propriedades personalizadas CSS sempre herdam por padrão. Em alguns casos, isso pode levar a componentes sendo estilizados de forma não planejada.

Como mostrei na seção anterior, você pode evitar isso resetando as propriedades individuais, o que impede valores desconhecidos de serem aplicados aos filhos de um elemento:

.MyComponent {
  --propertyName: initial;
}

Embora não faça parte da especificação ainda, a propriedade está sendo discutida (2), o que poderia ser usado para redefinir todas as propriedades personalizadas. E se você quiser fazer a whitelist em apenas algumas propriedades, você pode configurar individualmente para herança, o que permitiria continuar a operar normalmente:

.MyComponent {
  /* Resets all custom properties. */
  --: initial;

  /* Whitelists these individual custom properties */
  --someProperty: inherit;
  --someOtherProperty: inherit;
}

Gerenciando nomes globais

Se você estiver prestando atenção em como eu nomeio minhas propriedades personalizadas, já deve ter percebido que eu prefixo propriedades de componentes específicos com o nome da classe do componente, por exemplo –Button-backgroundColor.

Como a maioria dos nomes em CSS, as propriedades personalizadas são globais e há sempre a possibilidade de que elas irão entrar em conflito com os nomes sendo usados ​​por outros desenvolvedores de sua equipe.

Uma maneira fácil de evitar esse problema é se ater a uma convenção de nomenclatura, como eu fiz aqui.

Para projetos mais complexos, você provavelmente gostaria de considerar algo como CSS Modules que localizam todos os nomes globais e manifestaram recentemente interesse em apoiar as propriedades personalizadas.

Encerrando

Se você não estava familiarizado com propriedades personalizadas no CSS antes de ler este artigo, espero ter convencido você a dar uma chance a elas. E se você era uma das pessoas céticas em relação à sua necessidade, espero que eu tenha mudado sua opinião.

As propriedades personalizadas trazem um novo conjunto de capacidades dinâmicas e poderosas para CSS, e eu tenho certeza de que muitos de seus maiores pontos fortes ainda serão descobertos.

As propriedades personalizadas preenchem uma lacuna que variáveis pré-processadas ​simplesmente não conseguem. Apesar disso, as variáveis ​​pré-processadas continuam a ser a escolha mais fácil de usar e a mais elegante, em muitos casos. E, por isso, acredito firmemente que muitos sites irão utilizar uma combinação de ambas no futuro. As propriedades personalizadas para variáveis ​​divididas por temas e pré-processamento dinâmico para modelagem estática.

Eu não acho que tem que ser uma situação “ou esse ou aquele”. E colocar um contra o outro como concorrentes é um um desserviço a todos.

Notas de rodapé:

  1. Você pode ativar Experimental Web Platform Features no Chrome, navegando para o endereço about:flags, procurando por “Experimental Web Platform Features”, e clicando no botão “Ativar”.
  1. O uso da propriedade (como ele se relaciona com elementos de estilo personalizados) foi mencionado por Tab Atkins neste comentário no GitHub. Além disso, em um texto na lista de discussão www-style, Tab sugeriu que a adição do à especificação deve acontecer em breve.

***

Philip Walton faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://philipwalton.com/articles/why-im-excited-about-native-css-variables/