Detalhes invisíveis que fazem interfaces parecerem certas
Matheus Cardoso
Quase nada disso aqui alguém percebe. Um botão que responde do jeito que você esperava, uma transição que não pede atenção nenhuma: o que fica é só a sensação de que funciona. Por trás são algumas centenas de decisões pequenas, e o prêmio por acertar todas é ninguém conseguir apontar uma.
Por que detalhes importam
Hoje quase todo software é bom o suficiente, e é justamente por isso que o gosto decide. As pessoas escolhem ferramenta pela sensação do conjunto, não pela lista de recursos. Beleza continua sendo pouco usada em software, o que faz dela uma vantagem barata.
Gosto se treina. Você constrói o seu convivendo com trabalho melhor que o seu e perguntando, toda vez que alguma coisa parecer boa, o que exatamente está fazendo aquilo.
“All those unseen details combine to produce something that's just stunning, like a thousand barely audible voices all singing in tune.”Paul Graham
Animações
Nem tudo precisa animar. Se o usuário vê uma ação 100 vezes por dia, não anime. Se vê ocasionalmente (modais, drawers), anime. A regra: animações de UI devem ficar abaixo de 300ms.
Nunca use ease-in para animações de UI: começa devagar, fazendo a interface parecer lenta. Use ease-out ou curvas customizadas mais fortes:
:root {
/* Strong ease-out for UI interactions */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
/* Strong ease-in-out for on-screen movement */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
/* Icon crossfade curve */
--ease-icon: cubic-bezier(0.2, 0, 0, 1);
}Scale on press
Um sutil scale(0.96) no clique dá aos botões feedback tátil. Sempre use 0.96. Nunca abaixo de 0.95, qualquer coisa menor parece exagerado. Experimente:
.button {
transition: scale 150ms ease-out;
}
.button:active {
scale: 0.96;
}Transições de ícones
Quando ícones mudam de estado (menu → X, link → check), não apenas troque a visibilidade. Anime com opacity, scale e blur: a desfocagem mascara a transição imperfeita entre duas formas distintas, criando a ilusão de uma transformação suave.
Os valores exatos: scale de 0.25 para 1, blur de 4px para 0px, opacidade de 0 para 1. Sem motion library, use CSS transitions com cubic-bezier(0.2, 0, 0, 1):
.icon-swap {
display: grid;
place-items: center;
}
.icon-swap > * {
grid-area: 1 / 1;
transition: opacity 300ms cubic-bezier(0.2, 0, 0, 1),
transform 300ms cubic-bezier(0.2, 0, 0, 1),
filter 300ms cubic-bezier(0.2, 0, 0, 1);
}
.icon-visible {
opacity: 1;
transform: scale(1);
filter: blur(0px);
}
.icon-hidden {
opacity: 0;
transform: scale(0.25);
filter: blur(4px);
}Entrada escalonada
Não anime um container inteiro. Quebre o conteúdo em pedaços semânticos e escalone cada um com ~100ms de delay. Combine opacity, translateY e blur para o efeito de entrada:
.stagger-item {
opacity: 0;
transform: translateY(12px);
filter: blur(4px);
animation: fadeInUp 400ms ease-out forwards;
}
.stagger-item:nth-child(1) { animation-delay: 0ms; }
.stagger-item:nth-child(2) { animation-delay: 100ms; }
.stagger-item:nth-child(3) { animation-delay: 200ms; }
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
filter: blur(0);
}
}Superfícies
Border-radius concêntrico
Quando elementos arredondados estão aninhados, o raio externo deve ser igual ao raio interno mais o padding entre eles: outer = inner + padding. Raios iguais em pai e filho é a coisa mais comum que faz interfaces parecerem erradas.
/* outerRadius = innerRadius + padding */
.card {
border-radius: 20px;
padding: 8px;
}
.card-inner {
border-radius: 12px; /* 20 - 8 = 12 ✓ */
}Sombras ao invés de bordas
Para cards, botões e containers, prefira box-shadow em camadas ao invés de bordas sólidas. Sombras se adaptam a qualquer fundo via transparência. Bordas sólidas não.
:root {
--shadow-border:
0px 0px 0px 1px rgba(0, 0, 0, 0.06),
0px 1px 2px -1px rgba(0, 0, 0, 0.06),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
}
/* Dark mode: single white ring */
.dark {
--shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.08);
}Tipografia
Três regras, e cada uma é uma linha de CSS: text-wrap: balance em headings, para quebras equilibradas; text-wrap: pretty em parágrafos, para não sobrar órfã; e font-variant-numeric: tabular-nums em qualquer número que muda, para o layout parar de pular.
Aplique -webkit-font-smoothing: antialiased no root para texto mais nítido no macOS.
Performance
Anime apenas transform e opacity: essas propriedades pulam layout e pintura, rodando na GPU. Animar width, height, padding ou margin dispara os três passos de renderização.
Nunca use transition: all. Sempre especifique as propriedades exatas:
/* Good: only animate what changes */
.card {
transition: transform 200ms ease-out, opacity 200ms ease-out;
}
/* Bad: transition everything */
.card {
transition: all 200ms ease-out;
}Skill file
Essas recomendações estão compiladas em um skill file para Claude Code, Cursor e ferramentas similares. Funciona com Laravel Blade, Livewire e qualquer projeto com CSS:
Baseado nos princípios de Emil Kowalski, Ibelick e Jakub Krehel.