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.

100+/dayNunca anime
OcasionalAnimação padrão

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);
}
ease-in
ease-out
custom

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):

click to toggle
.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.

concentric
same radius
/* 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.

shadow
border
: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.

transform, opacityGPU: · Anime livremente
width, height, marginGPU: · Evite

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:

npx skills add matheuscarddoso/blade-ui-skill

Baseado nos princípios de Emil Kowalski, Ibelick e Jakub Krehel.