Detalles invisibles que hacen que las interfaces se sientan bien

Matheus Cardoso

La mayoría de los detalles de una interfaz los usuarios nunca los notan conscientemente. Ese es el punto. Cuando un botón responde exactamente como alguien espera, cuando una transición no llama la atención, cuando todo simplemente funciona — eso es el resultado de cientos de decisiones invisibles trabajando juntas.


Por qué importan los detalles

En un mundo donde todo software es "suficientemente bueno," el gusto es el diferencial. Las personas eligen herramientas basándose en la experiencia general, no solo en la funcionalidad. La belleza está subutilizada en software — úsala como palanca.

El buen gusto no es preferencia personal. Es un instinto entrenado: la habilidad de ver más allá de lo obvio y reconocer lo que eleva. Lo desarrollas rodeándote de trabajo excelente y pensando profundamente sobre por qué algo se siente bien.

“All those unseen details combine to produce something that's just stunning, like a thousand barely audible voices all singing in tune.”— Paul Graham

Animaciones

No todo necesita animarse. Si el usuario ve una acción 100 veces al día, no la anime. Si la ve ocasionalmente (modales, drawers), anime. La regla: las animaciones de UI deben estar por debajo de 300ms.

100+/dayNunca anime
OcasionalAnimación estándar

Nunca use ease-in para animaciones de UI — empieza lento, haciendo que la interfaz se sienta perezosa. Use ease-out o curvas personalizadas más fuertes:

: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

Un sutil scale(0.96) al hacer clic da a los botones feedback táctil. Siempre use 0.96. Nunca por debajo de 0.95 — cualquier cosa menor se siente exagerado. Pruébelo:

.button {
  transition: scale 150ms ease-out;
}

.button:active {
  scale: 0.96;
}

Transiciones de íconos

Cuando los íconos cambian de estado (menú → X, enlace → check), no solo cambie la visibilidad. Anime con opacity, scale y blur — el desenfoque enmascara la transición imperfecta entre dos formas distintas, creando la ilusión de una transformación suave.

Los valores exactos: scale de 0.25 a 1, blur de 4px a 0px, opacidad de 0 a 1. Sin librería de motion, use transiciones CSS con 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

No anime un container entero. Divida el contenido en bloques semánticos y escalone cada uno con ~100ms de delay. Combine opacity, translateY y blur para el efecto 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);
  }
}

Superficies

Border-radius concéntrico

Cuando elementos redondeados están anidados, el radio externo debe ser igual al radio interno más el padding entre ellos: outer = inner + padding. Radios iguales en padre e hijo es lo más común que hace que las interfaces se sientan mal.

concentric
same radius
/* outerRadius = innerRadius + padding */
.card {
  border-radius: 20px;
  padding: 8px;
}
.card-inner {
  border-radius: 12px; /* 20 - 8 = 12 ✓ */
}

Sombras en vez de bordes

Para cards, botones y containers, prefiera box-shadow en capas sobre bordes sólidos. Las sombras se adaptan a cualquier fondo via transparencia — los bordes sólidos no.

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);
}

Tipografía

Tres reglas simples que hacen toda la diferencia: text-wrap: balance en headings para quiebres de línea equilibrados, text-wrap: pretty en párrafos para evitar huérfanos, y font-variant-numeric: tabular-nums en cualquier número que cambia dinámicamente para evitar layout shift.

Aplique -webkit-font-smoothing: antialiased en el root para texto más nítido en macOS.


Rendimiento

Solo anime transform y opacity — estas propiedades saltan layout y pintura, corriendo en la GPU. Animar width, height, padding o margin dispara los tres pasos de renderizado.

transform, opacityGPU: Anime libremente
width, height, marginGPU: Evite

Nunca use transition: all. Siempre especifique las propiedades exactas:

/* 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

Estas recomendaciones están compiladas en un skill file para Claude Code, Cursor y herramientas similares. Funciona con Laravel Blade, Livewire y cualquier proyecto con CSS:

npx skills add matheuscarddoso/blade-ui-skill

Basado en los principios de Emil Kowalski, Ibelick y Jakub Krehel.