Novo Livro!
Categorias
Projetos
CSS

Fallbacks para a função CSS progress() em todos os navegadores

Fallbacks para a função CSS progress() em todos os navegadores

A função CSS progress() diz em que ponto um valor está entre dois limites, com um número de 0 a 1. Enquanto o suporte não chega a todos os navegadores, a mesma conta pode ser feita com calc(), clamp(), o truque do tan(atan2()) e @property, e o fallback certo depende só das unidades envolvidas.

A nova função CSS progress() calcula em que ponto um valor está entre um limite inicial e um final, e devolve um número sem unidade entre 0 e 1.

Enquanto o suporte dos navegadores não cresce, dá para calcular o progresso manualmente, usando a mesma mecânica que está por trás da função.

A matemática de progress(value, start, end) é simples:

progresso = (valor atual - início) / (fim - início)

Isso também é conhecido como normalização.

Reta numérica de 320px a 1200px com o valor 500px marcado. Acima da reta, os resultados: 0 no início, aproximadamente 0,2045 no valor e 1 no fim. Abaixo, o offset de 500 menos 320, igual a 180px, e o span de 1200 menos 320, igual a 880px. 180 dividido por 880 dá aproximadamente 0,2045.

A conta do progress(): a distância do início até o valor (offset) dividida pela distância do início até o fim (span). Fora do intervalo, o resultado fica preso em 0 ou 1.

Dependendo do tipo de valor com que você trabalha, implementar isso em CSS vai de um simples calc() até alguns truques de CSS mais complicados.

Só números sem unidade

Se você trabalha apenas com números sem unidade (como no artigo de Preethi Sam), dá para aplicar a fórmula direto no calc(). Para imitar o comportamento padrão de progress(), o exemplo usa clamp().

Por exemplo, para descobrir onde 50 fica entre 10 e 100:

--progress: clamp(0, (50 - 10) / (100 - 10), 1);

Pronto, quase tão fácil quanto progress().

Trabalhando com unidades

Na maioria das vezes, a ideia é usar unidades como px, rem ou vw e, mesmo assim, obter um valor sem unidade.

Navegadores que suportam aritmética tipada (typed arithmetic) já fazem isso. As duas linhas a seguir devem produzir o mesmo resultado:

--progress: clamp(0, (100vw - 20rem)/(100rem - 20rem), 1);
--progress: progress(100vw, 20rem, 100rem);

Infelizmente, isso ainda não funciona em todos os navegadores (olhando para você, Firefox).

É aí que entra o truque do tan(atan2()).

Daria para envolver cada valor nele, mas é possível otimizar mais.

A fórmula do progresso exige uma divisão, e atan2(y, x) já divide seus valores. Passe o offset (o deslocamento, valor - início) como primeiro argumento e o span (a extensão do intervalo, fim - início) como segundo.

Se todos os valores usam a mesma unidade, dá para colocá-los direto:

--progress: tan(atan2(500px - 320px, 1200px - 320px));
Triângulo retângulo com o cateto horizontal x igual ao span de 880px e o cateto vertical y igual ao offset de 180px. Três passos: 1, subtração, 500px menos 320px dá 180px e 1200px menos 320px dá 880px; 2, atan2(180px, 880px) dá um ângulo de aproximadamente 11.56deg; 3, tan(11.56deg) dá aproximadamente 0.2045, um número sem unidade.

atan2() recebe dois comprimentos e devolve o ângulo do triângulo formado por eles. tan() transforma esse ângulo de volta na razão y ÷ x, já sem unidade.

Misturando unidades

Normalmente, as unidades não vão bater. Em teoria, atan2() deveria lidar com unidades misturadas, e em alguns navegadores lida. Mas, para funcionar em todos, é preciso um pouco mais de trabalho.

Veja o caso da tipografia fluida.

Na tipografia fluida, o valor de progresso deve se basear na largura da viewport (100vw), em relação a um limite inferior e um superior (320px e 1200px).

Aqui vw e px estão misturados, então é preciso converter 100vw para pixels para a conta funcionar em todo lugar.

Para isso, registre a largura da viewport com @property, como uma custom property do tipo <length> com 100vw de valor inicial:

@property --vw {
syntax: "<length>";
inherits: true;
initial-value: 100vw;
}
:root {
--progress: clamp(0, tan(atan2(var(--vw) - 320px, 1200px - 320px)), 1);
}

Registrar --vw como <length> faz o navegador resolver 100vw para o equivalente em px. Agora todos os valores passados para atan2() compartilham a mesma unidade, e a conta funciona em todos os navegadores.

Comparação numa janela de 800px. Sem registro, --vw: 100vw é só um pedaço de texto, var(--vw) vira 100vw e o atan2 recebe vw e px misturados, o que funciona em alguns navegadores e em outros não. Registrada com @property como length, --vw é computada como 800px e o atan2 recebe atan2(480px, 880px), com tan de aproximadamente 0.5455.

Sem registro, var(--vw) só repassa o texto 100vw. Registrada como <length>, a custom property chega ao atan2() já convertida em pixels.

A abordagem robusta

E se você quiser usar rem nos limites?

Registrar uma custom property como <length> resolve o valor para pixels; não dá para forçar a resolução para rem. Para não misturar unidades, seria preciso registrar todos os valores, de modo que todos virassem pixels.

Felizmente, isso pode ser otimizado. Não é preciso registrar valor, início e fim separadamente, apenas os dois valores passados para atan2(): o offset e o span.

Basta transformar esses valores em custom properties e registrá-los com @property:

@property --offset {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
@property --span {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
:root {
--offset: calc(100vw - 20rem);
--span: calc(80rem - 20rem);
--progress: clamp(0, tan(atan2(var(--offset), var(--span))), 1);
}

E pronto: a mesma conta de progress(), com um suporte muito melhor entre os navegadores.

Para imitar a versão sem limites (unclamped) de progress(), remova o clamp().

Qual método usar?

A abordagem depende das unidades em uso:

  • Só números sem unidade: calcule o valor direto com calc() ou clamp().
  • Com unidades: use o truque do tan(atan2()).
  • Uma unidade só: não é preciso registrar nenhuma custom property.
  • Pixels + uma unidade que não seja pixel: registre o valor que não está em pixels como <length>. É o caso mais comum na tipografia fluida.
  • Várias unidades que não são pixel: transforme o offset e o span em custom properties, registre as duas como <length> e passe-as para atan2().

A função progress() já está nos navegadores modernos, então não vai demorar até dar para usá-la nativamente sem preocupação. Até lá, estes fallbacks ajudam a escolher a saída mais simples para cada situação.