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 --outdent
propriedade personalizada em contêineres de composição. A --outdent
variá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
--outdent
para 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 0
de 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 figure
elementos 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
margin
valor deve ser um<length>
- Um
background-color
deve ser um<color>
- Um
animation-duration
deve ser um<time>
- A
line-height
pode 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 1
se torna um <length>
se você adicionar unidades de comprimento ( 1px
, 1em
, etc) e um <time>
se você adicionar unidades de tempo ( 1s
, 1ms
, etc).
Em alguns casos, 1
pode até ser um <string>
. Embora os contadores CSS estejam claramente contando com números, a saída de counter()
e 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 content
propriedade, mas a lógica disso é um pouco recursiva – se a única saída for a <string>
, e só content
aceitar <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 px
e zero %
e assim por diante. Você não pode definir margin
para 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 margin
ou 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 amboswidth: 0;
ewidth: 5px;
sejam válidos.
Corrigindo o caso de uso outdent
Este é o problema com o meu --outdent
có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.