Novo Livro!
Categorias
Ferramentas
Projetos
CSS

Imperfeição divertida com a função random() do CSS

Imperfeição divertida com a função random() do CSS

A função random() do CSS sorteia valores direto na folha de estilo, sem biblioteca JavaScript e sem loops de pré-processador. Com ela, letras ganham inclinações sutis e cards caem na tela como fotos jogadas sobre a mesa, sempre dentro dos limites que o design system define.

Abaixo estão a sintaxe, o suporte atual e seis demos que mostram onde essa aleatoriedade faz sentido (e onde não faz).

Em design, adoramos passar horas obcecados por consistência, grids e alinhamento.

Mas quando chegou a notícia de que a função nativa random() do CSS tinha chegado ao Safari e estava sendo liberada no Chrome 156, surgiu a dúvida: onde a aleatoriedade faz sentido de verdade na web? É algo que queremos no nosso design system?

Para descobrir, o autor, Brecht De Ruyte, fez o que sempre faz na dúvida: montou um monte de demos divertidas no CodePen, explorando tipografia orgânica, letras saltitantes, cards em leque e um fanzine punk.

Para quem gosta de construir design systems e de mexer com arquitetura CSS, boa parte do trabalho gira em torno de controle. Definimos escalas de espaçamento, travamos a tipografia, criamos tokens de cor e garantimos que tudo se alinhe num grid.

Consistência costuma ser o arroz com feijão de quem trabalha como design engineer, e um selo de competência na hora de criar design systems. Se um card de repente pula 20 pixels para a esquerda ao recarregar a página, não é expressão criativa: é um bug que o gerente de projeto vai registrar.

Por isso, a primeira reação à notícia de que a função nativa random(), da especificação CSS Values and Units Level 5, foi lançada no Safari e está sendo liberada no Chrome 156 (com sinais positivos da Mozilla para o Firefox também) foi: que legal!

A segunda reação foi: espera aí… onde usar isso sem fazer bagunça? Como convencer designers de que o aleatório pode ser uma boa ideia para um design system?

Porque, sejamos honestos: por que raios iríamos querer aleatoriedade nas folhas de estilo depois de passar a última década tentando deixar a web mais previsível? Mesmo usando flexbox ou tipografia fluida, ainda tentamos deixar tudo o mais alinhado e uniforme possível.

O charme das coisas que não estão 100% retas

Afaste-se do navegador por um segundo e olhe para os objetos físicos ao seu redor: nada é realmente uniforme.

Pense numa impressão antiga em tipografia: a tinta escorre um pouco nas fibras do papel, algumas letras têm uma inclinação microscópica e a linha de base nem sempre é tão reta.

Ou num punhado de Polaroids largadas sobre a mesa de centro; essas fotos não são perfeitas, e provavelmente também não estão retas. Ou nas luzes piscando numa rua à noite.

Essa pitada de bagunça analógica é o que faz as coisas parecerem vivas, orgânicas e humanas.

Na web, ficamos tão bons em perfeição de pixel que, às vezes, as interfaces acabam parecendo meio estéreis.

Isso fica ainda mais evidente agora, com designs criados às pressas por um LLM sem direção: geralmente acabamos com o mesmo tipo de design de 10 anos atrás. Tudo quadradinho, alinhado, arrumado. Funciona, mas, francamente, fica um pouco chato.

Aleatoriedade no CSS não precisa significar jogar o design system pela janela. É sobre pequenas imperfeições e um toque lúdico. Talvez possamos até chamar isso de diversão.

Para ver como isso fica na prática, foram algumas horas mexendo no CodePen. As demos podem parecer um pouco isoladas, mas muitos designers certamente conseguiriam tirar o melhor desse caos controlado. Antes de chegar a elas, porém, vale olhar a sintaxe.

Como a função random() do CSS funciona

A função random() é especificada no CSS Values and Units Module Level 5. Ela gera um valor pseudoaleatório dentro do próprio CSS, sem precisar de JavaScript nem de um loop de Sass em tempo de build.

A especificação formal é assim:

random( <random-key>?, <min>, <max>, <step>? )

Na prática, e nas implementações atuais dos navegadores, basta escrever os argumentos separados por vírgula:

/* Ângulo aleatório entre -3deg e 3deg */
rotate: random(-3deg, 3deg);
/* Largura aleatória entre 20px e 80px, em passos de 10px */
inline-size: random(20px, 80px, 10px);
/* Duração de animação aleatória */
animation-duration: random(1.5s, 3s, 0.2s);
Diagrama desenhado à mão da chamada random(--tilt element-scoped, -3deg, 3deg, 0.5deg), com cada argumento destacado e ligado a um cartão: chave de cache (opcional), mínimo, máximo e passo (opcional)
Os quatro argumentos de random(): só o mínimo e o máximo são obrigatórios.

Unidades correspondentes

Não deve surpreender quase ninguém, mas vale pelo menos mencionar: o mínimo e o máximo precisam ter exatamente o mesmo tipo de unidade.

O parser do CSS precisa saber com que dimensão está lidando. Dá para usar comprimentos, ângulos, tempo, porcentagens ou números sem unidade:

/* Certo: os dois são ângulos */
rotate: random(-4deg, 4deg);
/* Certo: os dois são números sem unidade */
opacity: random(0.3, 1);
/* Não! Não dá para misturar unidades */
inline-size: random(2rem, 100px); /* Inválido */

Se algum dia for preciso combinar unidades, a saída é recorrer a calc() dentro dos argumentos, para que cada lado seja resolvido corretamente.

Passo e arredondamento

O terceiro argumento opcional é o tamanho do passo. Sem ele, o navegador escolhe um valor num intervalo contínuo. Com um passo, o valor aleatório é arredondado para incrementos discretos:

/* Só produz 0px, 15px, 30px, 45px ou 60px */
top: random(0px, 60px, 15px);

Isso ajuda quando se quer que as coisas pulem um pouco, mas ainda caiam no grid predefinido.

Chaves de cache: mantendo tudo em sincronia

Normalmente, cada vez que random() é avaliada numa propriedade, ela cria um valor aleatório independente. Mas pode haver um caso em que uma caixa define inline-size e block-size de forma aleatória e, ao mesmo tempo, os dois precisam coincidir para que o elemento continue um quadrado perfeito.

É aí que entra a chave de cache:

.card-thumbnail {
/* As duas propriedades compartilham a chave --square-size */
inline-size: random(--square-size element-scoped, 60px, 120px);
block-size: random(--square-size element-scoped, 60px, 120px);
}

Na versão mais recente da especificação, um dashed ident sozinho, como --square-size, compartilha sua semente globalmente entre todos os elementos do documento (o que deixaria todos os cards idênticos).

Para manter largura e altura ligadas num único elemento, dando a cada card seu próprio tamanho independente, a especificação exige combiná-lo com a palavra-chave element-scoped (por exemplo, --square-size element-scoped).

Três cartões desenhados à mão comparando random() em largura e altura: sem chave, os blocos viram retângulos de proporções diferentes; com --size element-scoped, cada bloco é um quadrado de tamanho próprio; só com --size, os três quadrados ficam idênticos
Sem chave, com chave e element-scoped, e só com a chave: o que muda em três cards.

Para ver isso em ação, há um pequeno exemplo. Na linha de cima, os tiles têm dimensões sem cache, o que distorce cada um num retângulo imprevisível e desencontrado.

Na linha de baixo, inline-size e block-size compartilham --square-size, garantindo que cada tile continue um quadrado 1:1 impecável. Recarregue o CodePen algumas vezes e repare que os cards de baixo mantêm a mesma aleatoriedade:

/* Sem cache: largura e altura são resolvidas de forma independente, criando retângulos aleatórios */
.tile-uncached {
inline-size: random(4.2rem, 8.5rem);
block-size: random(4.2rem, 8.5rem);
rotate: random(-3.5deg, 3.5deg);
}
/* Com cache: a chave compartilhada garante um quadrado 1:1 limpo */
.tile-cached {
inline-size: random(--square-size element-scoped, 4.2rem, 8.5rem);
block-size: random(--square-size element-scoped, 4.2rem, 8.5rem);
rotate: random(--tilt element-scoped, -3.5deg, 3.5deg);
}

Suporte dos navegadores e progressive enhancement

Onde dá para testar isso hoje?

  • WebKit / Safari: o Safari foi o primeiro a implementar.
  • Chrome / Edge: lançado e sendo liberado agora mesmo, com o Chrome 156.
  • Firefox: sinais positivos no processo de padronização, então tomara que não demore muito.

Como ainda estamos na fase de liberação, o ideal é envolver os experimentos em @supports:

/* Base para todos os navegadores */
.badge {
rotate: 0deg;
}
/* Versão aprimorada para navegadores com random() */
@supports (rotate: random(-2deg, 2deg)) {
.badge {
rotate: random(-2.5deg, 2.5deg);
}
}

A futura função random-item()

Há também outra função na especificação, chamada random-item(). Enquanto random() lida com intervalos numéricos, random-item() escolhe um item de uma lista de tokens ou palavras-chave predefinidos:

/* Escolhendo uma cor aleatória da paleta */
background-color: random-item(--seed, oklch(75% 0.18 150), oklch(65% 0.22 350), oklch(85% 0.16 85));

Isso pode ser útil para sortear dentro de um conjunto de design tokens. Ela ainda não tem suporte em nenhum navegador, mas certamente é algo para ficar de olho.

E agora, a parte prática.

Demo 1: vamos simplesmente randomizar letras

Na época dos primeiros testes com o recurso, o autor também estava fazendo uns truques tipográficos com animações. Por isso o primeiro pensamento foi: tipografia! E se adicionássemos só um pouquinho de imperfeição a uma palavra?

Pense em capas de livros antigos. As letras não estão desalinhadas por quilômetros, mas têm pequenas imperfeições.

Este é o CSS que move as letras:

@layer demo {
.char {
display: inline-block;
/* Uma inclinação mínima */
rotate: random(-2.8deg, 2.8deg);
/* Um micro empurrão em X e Y */
translate: random(-1.5px, 1.5px) random(-2.8px, 2.8px);
/* Variação bem sutil de tinta */
opacity: random(0.8, 1, 0.02);
transition-property: rotate, translate, opacity;
transition-duration: 0.3s;
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Endireita no hover */
.word:hover .char {
rotate: 0deg;
translate: 0 0;
opacity: 1;
}
}

Também há um pequeno efeito de hover. Durante a leitura, a palavra parece brincalhona. Mas, no instante em que o cursor passa sobre ela, cada caractere volta para o lugar.

Ferramenta do dpw Gerador de easing CSS A volta das letras usa uma curva cubic-bezier() que passa do ponto e retorna. Monte e compare curvas assim no gerador de easing. Usar a ferramenta

Por favor, não quebre o leitor de tela

Um rápido alerta de acessibilidade: ao quebrar uma palavra em elementos <span> separados para estilizar cada letra, os leitores de tela muitas vezes leem caractere por caractere: “i - m - p - e - r - f - e - c - t - i - o - n”, ou fazem pausas estranhas entre cada um. É uma péssima experiência para o usuário.

Sempre esconda os spans visuais das tecnologias assistivas e forneça a palavra completa (com ARIA):

<h1 class="word" aria-label="Imperfection">
<span class="word-inner" aria-hidden="true">
<span class="char">i</span>
<span class="char">m</span>
<span class="char">p</span>
<span class="char">e</span>
<span class="char">r</span>
<span class="char">f</span>
<span class="char">e</span>
<span class="char">c</span>
<span class="char">t</span>
<span class="char">i</span>
<span class="char">o</span>
<span class="char">n</span>
</span>
</h1>

Quem usa leitor de tela recebe o texto limpo, e quem enxerga recebe as peculiaridades visuais divertidas.

Demo 2: animando as letras com um bounce procedural puro

Com aquelas letras paradas ali com suas pequenas inclinações, foi impossível resistir: e se elas fossem animadas?

O passo seguinte foi brincar com um keyframe de bounce. Era só diversão, mas o resultado final arrancou um sorriso. Se cada letra for animada exatamente com o mesmo timing, o resultado é aquela onda senoidal limpa que todo mundo já viu.

Mas se cada letra ganha sua própria altura de pulo, sua própria duração e seu próprio achatamento na aterrissagem, de repente a palavra parece um bando de personagenzinhos pulando numa cama elástica.

Veja como os parâmetros do bounce estão ligados:

@layer demo {
.char {
display: inline-block;
transform-origin: center bottom;
/* Variáveis do bounce procedural */
--jump: random(element-scoped, -35px, -55px, 2px);
--tilt: random(element-scoped, -8deg, 8deg, 0.5deg);
--dur: random(element-scoped, 1s, 1.2s, 0.1s);
--delay: random(element-scoped, -3s, 0s, 0.1s);
--rebound-squash: random(element-scoped, 1.08, 1.18, 0.02);
animation: letter-bounce var(--dur) linear var(--delay) infinite;
}
@keyframes letter-bounce {
/* ápice */
0% {
translate: 0 var(--jump);
rotate: var(--tilt);
scale: 1 1;
animation-timing-function: cubic-bezier(0.65, 0.05, 0.85, 0.35);
}
/* aterrissagem */
44% {
translate: 0 0;
rotate: calc(var(--tilt) * 0.2);
scale: 0.96 1.05;
animation-timing-function: ease-out;
}
/* achatamento */
52% {
translate: 0 0;
rotate: 0deg;
scale: var(--rebound-squash) calc(2 - var(--rebound-squash));
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}
/* subindo de novo */
62% {
translate: 0 calc(var(--jump) * 0.2);
rotate: calc(var(--tilt) * -0.4);
scale: 0.98 1.03;
animation-timing-function: cubic-bezier(0.15, 0.85, 0.35, 1);
}
100% {
translate: 0 var(--jump);
rotate: var(--tilt);
scale: 1 1;
}
}
}

Repare no delay negativo da animação (--delay: random(-3s, 0s)).

Com um delay positivo, as letras ficam congeladas no carregamento da página, esperando seus timers acabarem antes de pular. Com um delay negativo, o navegador finge que a animação já está rodando há alguns segundos. No momento em que a página carrega, cada letra já está pulando no seu próprio ritmo.

Demo 3: cards inclinados e sombras dinâmicas

Já construímos cards. Muitos cards. Cards demais… Grids de portfólio, resumos de blog, listas de recursos. E, na maior parte do tempo, eles ficam em três ou quatro colunas arrumadinhas e sem graça.

Para dashboards, é exatamente isso que se quer. Mas, num portfólio de lifestyle, num site infantil ou numa coleção de fotos de férias, dá para ser um pouco mais brincalhão. Foi o que o exemplo a seguir fez:

Veja como isso foi montado:

@layer demo {
.card {
/* Uma inclinação aleatória suave e um deslocamento vertical */
--tilt: random(-3.5deg, 3.5deg);
--shift-y: random(-6px, 6px);
rotate: var(--tilt);
translate: 0 var(--shift-y);
/* A sombra acompanha o ângulo da inclinação! */
box-shadow:
calc(var(--tilt) * 1.5)
calc(10px + abs(var(--tilt) * 1px))
28px
oklch(20% 0.02 260 / 0.07);
transition:
rotate 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
translate 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 0.4s ease;
/* Endireita no hover ou no foco pelo teclado */
&:hover,
&:has(.button:focus-visible) {
rotate: 0deg;
translate: 0 -10px;
z-index: 10;
box-shadow:
0 20px 44px oklch(20% 0.02 260 / 0.12),
0 2px 6px oklch(20% 0.02 260 / 0.04);
}
}
}

No exemplo, brincar com a sombra foi um experimento divertido, e ainda há mais efeitos desse tipo para testar no futuro.

Demo 4: o baralho em leque com sibling-index e scroll

É aqui que os recursos modernos do CSS começam a ficar realmente interessantes. Na demo anterior, com os cards inclinados, usamos aleatoriedade pura. Mas e se houver uma pilha de cards que precisa se abrir em leque pela tela conforme a rolagem?

Neste exemplo, a escolha foi usar matemática para a estrutura e aleatoriedade para a imperfeição humana.

Esta é a configuração:

@layer demo {
.deck-card {
position: absolute;
/* Base matemática do leque via sibling-index() (centralizada no card do meio) */
--elevation: sibling-index();
--base-rot: calc((sibling-index() - (sibling-count() + 1) / 2) * 12deg);
--base-x: calc((sibling-index() - (sibling-count() + 1) / 2) * 60px);
--jitter-rot: random(-3.5deg, 3.5deg);
--jitter-y: random(-12px, 12px);
--jitter-x: random(-8px, 8px);
transform-origin: center 130%;
/* SDA */
animation: splay-fan linear both;
animation-timeline: scroll(root);
animation-range: 0% 80%;
transition: scale 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
&:hover {
scale: 1.05;
/* Item em hover fica por cima */
z-index: calc(sibling-count() + 1); }
}
@keyframes splay-fan {
from {
rotate: 0deg;
translate: 0 0;
box-shadow:
0 1px 3px oklch(20% 0.02 260 / 0.1),
0 2px 8px oklch(20% 0.02 260 / 0.07);
}
to {
rotate: calc(var(--base-rot) + var(--jitter-rot));
translate: calc(var(--base-x) + var(--jitter-x)) var(--jitter-y);
/* Em leque:
1. Sombra nítida na borda de contato entre os cards
2. Sombra atmosférica elevada, escalada de forma realista por sibling-index() */
box-shadow:
0
calc(1px + var(--elevation) * 0.6px)
4px
oklch(20% 0.02 260 / 0.1),
calc(var(--base-x) * 0.15)
calc(10px + var(--elevation) * 5px)
calc(18px + var(--elevation) * 7px)
oklch(20% 0.02 260 / 0.13);
}
}
}

Repare como isso funciona de forma limpa:

  • sibling-index() calcula o leque matemático para que os cards se espalhem de maneira uniforme em torno do card central, e escala --elevation para que os cards mais altos na pilha projetem sombras maiores.
  • calc((sibling-index() - (sibling-count() + 1) / 2) * 12deg); garante que a quantidade de cards não importa; cada um vai girar os mesmos 12deg.
  • random() adiciona aquela leve oscilação que se teria se uma pessoa distribuísse as cartas à mão.
  • animation-timeline: scroll(root) amarra todo o leque e a transição das sombras à barra de rolagem.

Demo 5: velocidade aleatória

Quem já viu sites brutalistas ou de moda de alto padrão provavelmente já reparou em textos horizontais, estilo letreiro, deslizando pela tela conforme a página rola.

Normalmente, desenvolvedores criam classes como .drift-slow, .drift-fast, .drift-reverse, ou usam um pesado listener de scroll em JavaScript para calcular os transforms.

Com random() e um pouquinho de matemática, dá para ter velocidades de deslocamento procedurais em poucas linhas (com scroll-driven animations):

Este é o CSS:

@layer demo {
.drift-line {
white-space: nowrap;
/* Alterna a direção entre as linhas usando paridade matemática */
--direction: calc(pow(-1, sibling-index()));
/* Distância percorrida (a velocidade) */
--travel: random(15%, 44%);
/* Amarrado à rolagem da página */
animation: tectonic-glide linear both;
animation-timeline: scroll(root);
animation-range: entry 0% cover 100%;
}
@keyframes tectonic-glide {
from {
translate: calc(var(--direction) * -1 * var(--travel)) 0;
}
to {
translate: calc(var(--direction) * var(--travel)) 0;
}
}
}

calc(pow(-1, sibling-index())) produz direções alternadas (1, -1, 1, -1), e random(15%, 44%) decide a velocidade de cada linha. Conforme a página rola, cada linha tipográfica se move no seu próprio ritmo.

Demo 6: aleatoriedade desfocada

O autor adora fotografia, e uma das coisas favoritas dele nas lentes fixas é a profundidade de campo rasa. Ao fotografar com uma abertura grande, como f/1.7, só o que está exatamente no plano de foco fica nítido. Todo o resto se dissolve num bokeh suave. Pois é, fotografia tem tudo a ver com esse bokeh!

Combinando random() com filter: blur(), opacity e scale, dá para recriar essa sensação de lente:

Este é o trecho:

@layer demo {
.focal-item {
/* Desfoque óptico, opacidade e escala aleatórios */
filter: blur(random(0px, 8px, 1px));
opacity: random(0.25, 1, 0.05);
scale: random(0.85, 1.25, 0.05);
transition:
filter 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
opacity 0.4s ease,
scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
/* Troca de foco no hover */
&:hover, &:focus {
filter: blur(0px);
opacity: 1;
scale: 1.15;
}
}
}

Algumas palavras caem em blur(0px) com opacidade total, bem diante dos olhos. Outras ficam desfocadas em 6px ou 8px e somem no fundo. E, ao passar o cursor sobre qualquer item desfocado lá atrás, o foco muda e ele fica nítido. É um ótimo efeitinho para seções hero decorativas ou nuvens de tags.

Onde a aleatoriedade faz sentido (e onde não faz)

Agora que já brincamos com o lado mais selvagem, vale falar de onde isso realmente cabe no trabalho do dia a dia.

Bons casos de uso

  • Microimperfeição sutil em UI decorativa: Polaroids inclinadas, ângulos leves em post-its ou títulos tipográficos de hero.
  • Dessincronizar loops de ambiente: sinalizadores de status, badges flutuando suavemente ou grids de partículas de fundo, em que o perfeito uníssono parece robótico.
  • Fundos generativos: campos de estrelas, comemorações com confete, círculos de bokeh flutuantes ou granulação de textura.
  • Direção de arte editorial: páginas de revista, flyers de eventos, layouts brutalistas e landing pages divertidas.
  • Camadas sobre a matemática: usar sibling-index() para o grid estrutural e adicionar pequenos deslocamentos aleatórios para dar calor humano.

Onde evitar

  • Layouts e grids principais: não randomize gutters de colunas, larguras de campos de formulário ou padding de containers. As pessoas precisam de uma geometria de layout previsível para navegar.
  • Texto corrido: não mexa em tamanhos de fonte, alturas de linha ou espaçamento entre letras em parágrafos longos. A fadiga de leitura é real.
  • Alvos interativos: não deixe botões ou links pequenos inclinados ou deslocados sem reposicioná-los no hover e no foco. Se o usuário precisa perseguir um botão que se mexe ou está torto, a interface falhou com ele.

Mas! O autor ficaria feliz em ser contrariado aqui. Fica o convite para desafiar o status quo e experimentar.

Sempre respeite prefers-reduced-motion

Sempre que houver animação, é obrigatório respeitar a preferência do usuário por movimento reduzido:

@media (prefers-reduced-motion: reduce) {
.char {
animation: none;
rotate: 0deg;
translate: 0 0;
}
}

Se alguém prefere movimento reduzido, também pode ser uma boa ideia achatar os ângulos, não só as animações. Muitos ângulos aleatórios também podem incomodar as pessoas durante a rolagem. O design deve ser divertido, mas nunca às custas da acessibilidade.

Entendendo de vez o random()

Mexer com random() nos últimos dias foi muito divertido. Ainda não dá para garantir que os amigos designers do autor vão se convencer, mas uma coisa é certa: a função traz um pouco de alegria.

Sempre que queríamos um pouco de aleatoriedade orgânica na web, era preciso fazer concessões: ou trazer uma biblioteca de animação em JavaScript, ou escrever enormes loops de pré-processador que cuspiam centenas de linhas de CSS estático.

Ter isso embutido no motor do navegador, sem nenhum custo de JavaScript, é uma adição muito bem-vinda.

Passamos a última década dominando consistência, tokens e design systems. Agora que essa base está pronta, talvez seja hora de deixar um pouco de bagunça divertida voltar para a web.

O autor acredita de verdade que a web deve ser divertida.

Esqueça os padrões; aqui está o random() do CSS.