Aguarde...

8 de agosto de 2022

Nem todos os zeros são iguais

Nem todos os zeros são iguais

Existe uma ‘melhor prática’ bem estabelecida de que os autores de CSS (assim como linters e minifiers) devem remover unidades de qualquer 0 valor. É uma boa regra na maioria dos casos, mas existem algumas situações comuns em que isso quebrará seu código.

Estou trabalhando em um redesenho do meu site pessoal e descobri que estava corrigindo o mesmo problema repetidamente. Faça a mudança, teste, confirme e então… por que está quebrado  novamente?

O comportamento pretendido

Ao definir a tipografia em um design, gosto de ‘recuar’ as listas – puxando os marcadores de lista (marcadores ou números) para a margem do documento, para que o conteúdo da lista se alinhe com o conteúdo em ambos os  lados.

Se você estiver lendo isso no site do OddBird com um navegador amplo o suficiente, verá que fazemos isso  aqui:

  • Em telas largas, os marcadores desta lista devem estar pendurados na  margem.
  • Mas em navegadores estreitos, não há ‘margem’ suficiente  disponível.
  • Para que os marcadores de lista permaneçam visíveis em telas pequenas, mudamos para o ‘hanging indent’ padrão do navegador – marcadores de lista alinhados ao conteúdo ao redor e o conteúdo da lista  recuado.

Existem várias maneiras de lidar com essa lógica de recuo/recuo (e consultas de contêiner podem ser úteis). Para o meu site, decidi configurar uma --outdentpropriedade personalizada em contêineres de composição. A --outdentvariável transmite se/quando e quanta margem está disponível para o  conteúdo:

main {
--outdent: 0;

@media (min-width: 40em) {
--outdent: -1em;
}
}
  • Por padrão, para telas pequenas, o --outdenté 0.
  • Quando um contêiner tem mais espaço, alterno --outdentpara algo como -1em.

Alguns elementos (como figuras) obtêm o recuo aplicado diretamente a uma  margem:

figure {
margin-inline-start: var(--outdent);
}

Mas a lógica da lista é um pouco mais complicada. Como os marcadores de lista ficam ‘fora da lista’ por padrão, precisamos 0de preenchimento de lista em telas grandes e preenchimento adicional em telas pequenas para obter um recuo. Eu faço isso com uma calc()função:

ol, ul {
/* (1em + 0 == 1em) and (1em + -1em == 0) */
padding-inline-start: calc(1em + var(--outdent));
}

li {
/* nested lists should not outdent */
--outdent: 0;
}

Haveria outras maneiras de fazer isso, é claro – mas fazia sentido para mim como uma maneira de lidar com diferentes estilos de saída com uma única  alternância de variável.

Infelizmente, o código acima não  funciona.

Por quê? Tudo parece certo, e meus figureelementos ficam como esperado – mas a lista nunca recua em telas pequenas. Olhando mais de perto, parece que toda a calc()função é considerada inválida . O que estou fazendo  de errado?

Tipos de valor CSS

CSS é uma linguagem ‘digitada’. Cada valor se enquadra em um dos vários ‘ tipos de dados ‘ – como um ‘número’ ou ‘comprimento’ ou ‘cor’. Existem muitos tipos diferentes em CSS, muitos deles específicos para as necessidades dos designers – e cada propriedade tem  requisitos de ‘tipo’ específicos:

  • Um marginvalor deve ser um<length>
  • Um background-colordeve ser um<color>
  • Um animation-durationdeve ser um<time>
  • line-heightpode ser a <number>ou a <length>, mas os dois tipos são tratados de forma  diferente

A única diferença entre a <number>, a <length>e a <time>está nas unidades aplicadas. Um <number>like 1se torna um <length> se você adicionar unidades de comprimento ( 1px1em, etc) e um <time>se você adicionar unidades de tempo ( 1s1ms, etc).

Em alguns casos, 1pode até ser um <string>. Embora os contadores CSS estejam claramente contando com números, a saída de counter()counters() sempre será uma <string>representação dessa contagem. Por enquanto, o mesmo vale para a saída da attr()função. Isso é parte do motivo pelo qual não podemos (atualmente) usar contadores e atributos para fazer muito fora do conteúdo gerado. (A outra razão é que essas funções só funcionam na contentpropriedade, mas a lógica disso é um pouco recursiva – se a única saída for a <string>, e só contentaceitar <string>valores, não há razão para permitir contadores em nenhum  outro lugar.)

E o CSS geralmente não permite coagir valores de um tipo para outro. Não há como pegar uma string e transformá-la em um número, ou vice-versa. Podemos converter um número em um comprimento (ou tempo) – calc(<number> * <length>)retornará um <length>valor – mas não podemos (ainda) ir para o outro lado:

.example {
--number: 3;
/* converts the number 3 to the length of 3em */
margin-block: calc(var(--number) * 1em);
}

Zero é (muitas vezes)  especial

Na maioria dos casos, zero é uma exceção às regras de tipo – podemos usá-lo em muitos lugares como a <number>ou a <length> sem adicionar nenhuma unidade! Isso porque 0é o mesmo comprimento (sem comprimento!), não importa quais unidades você aplique a ele. Zero emé o mesmo que zero pxe zero %e assim por diante. Você não pode definir marginpara 5(a <number>), mas pode definir para 0(também a <number>).

Para zero e apenas zero , podemos usar a <number>quando o CSS espera a <length>.

E com o tempo, isso se tornou uma ‘melhor prática’ – muitas vezes aplicada por linters e minificadores CSS . O raciocínio usual é o desempenho. Remover todas as unidades de zeros economizará alguns bytes para cada ocorrência. Você também pode considerá-lo melhor para legibilidade – se todos os valores zero forem iguais, as unidades apenas desviam o significado. 

Zero é (nem sempre)  especial

Essa ‘prática recomendada’ funciona muito bem para valores zero brutos, aplicados diretamente a propriedades como marginou padding– mas há outros lugares onde essa ‘prática recomendada’ quebrará seu CSS . 

Em geral: quando zero está dentro de uma função, o tipo de zero importa . (Pelo menos, é aí que eu sempre encontrei o  problema.)

Embora a rgb()função aceite valores <number>(0-255) ou <percentage>(0%-100%), você não tem permissão para combinar os dois  tipos:

html {
/* valid colors */
color: rgb(0 60 80);
color: rgb(0% 60% 80%);

/* invalid colors */
color: rgb(0% 60 80);
color: rgb(0 60% 80%);
}

Outras funções de cores têm requisitos mais rigorosos. Em hsl()apenas o valor de matiz pode ser a <number>ou <angle>, mas a luminosidade e a saturação devem ser  porcentagens:

html {
/* valid colors */
color: hsl(0 60% 80%);
color: hsl(0deg 60% 80%);

/* invalid colors */
color: hsl(60 0 80);
color: hsl(60deg 0 80%);
}

O mesmo acontece dentro da calc()função. Números podem ser adicionados ou subtraídos com outros números, e comprimentos podem ser adicionados ou subtraídos com outros comprimentos. É inválido adicionar ou subtrair um número com um comprimento . E isso é verdade mesmo que o número ou comprimento seja zero :

Acontece que isso também está documentado na especificação da calc()função:

Nota: Como <number-token>s são sempre interpretados como <number>s ou <integer>s, s “sem unidade” <length>não são suportados em funções matemáticas. Ou seja, width: calc(0 + 5px);é inválido, porque está tentando adicionar a <number>a a <length>, mesmo que ambos width: 0;width: 5px;sejam válidos.

Corrigindo o  caso de uso outdent

Este é o problema com o meu --outdentcódigo acima:

ol, ul {
/* (1em + 0 == 1em) and (1em + -1em == 0) */
padding-inline-start: calc(1em + var(--outdent));
}

li {
/* nested lists should not outdent */
--outdent: 0;
}

Quando --outdenté zero (sem nenhuma unidade), a calc()função se torna calc(0 + 1em)– a <number>sendo adicionado a a <length>– o que é inválido. A declaração inteira é ignorada e não paddingé aplicado.

A correção é simples: adicione unidades ao --outdent, mesmo quando o valor for  zero:

li {
/* any length units will work here */
--outdent: 0px;
}

E a razão pela qual continuo corrigindo esse mesmo problema repetidamente é porque uso um linter que não entende o problema. Esse linter é executado toda vez que eu compro meu código e apaga as unidades que eu  forneci.

Seja cauteloso com fiapos e  minificação

Dependendo do linter, provavelmente posso desativar essa ‘otimização’ específica – o Stylelint permite desativá-lo especificamente nas propriedades personalizadas . Isso é bom. Entendo que não é fácil contabilizar todos os casos extremos nas configurações padrão. Sempre haverá problemas que surgirão  .

Mas esses problemas são exacerbados por ferramentas como linters e minifiers que aplicam transformações opinativas ao  código já válido.

Eu tive um problema semelhante na semana passada, com um minifier CSS removendo todas as Cascade Layers do meu CSS . Em um caso, a transformação é uma ‘melhor prática’ muito ansiosa. No outro, é uma tentativa ansiosa de remover a ‘sintaxe desconhecida’. Em ambos os casos, gostaria que linters e minifiers fossem menos ansiosos para transformar meu  código .

Acho que nós (como indústria) tendemos a adotar regras e ‘melhores práticas’ muito rapidamente, sem comunicar as advertências claramente, e então deixamos de atualizar nosso entendimento à medida que as coisas mudam. Ferramentas baseadas nessas tendências de “melhores práticas” precisam ser escritas e também usadas com cautela. Eles geralmente não devem transformar a sintaxe desconhecida , que inclui valores dentro de propriedades personalizadas – onde praticamente qualquer valor é permitido e a finalidade da propriedade é  desconhecida.

Postado em Blog
Escreva um comentário