Invisible details that make interfaces feel right

Most interface details users never consciously notice. That is the point. When a button responds exactly as someone expects, when a transition doesn't draw attention, when everything just works — that is the result of hundreds of invisible decisions working together.

Why details matter

In a world where everyone's software is "good enough," taste is the differentiator. People select tools based on the overall experience, not just functionality. Beauty is underutilized in software — use it as leverage.

Good taste is not personal preference. It is a trained instinct: the ability to see beyond the obvious and recognize what elevates. You develop it by surrounding yourself with great work and thinking deeply about why something feels good.

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

Animations

Not everything needs to animate. If the user sees an action 100 times a day, don't animate it. If they see it occasionally (modals, drawers), animate. The rule: UI animations should stay under 300ms.

100+/dayNever animate
OccasionalStandard animation

Never use ease-in for UI animations — it starts slow, making the interface feel sluggish. Use ease-out or stronger custom curves:

: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

A subtle scale(0.96) on click gives buttons tactile feedback. Always use 0.96. Never below 0.95 — anything smaller feels exaggerated. Try it:

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

.button:active {
  scale: 0.96;
}

Icon transitions

When icons change state (menu → X, link → check), don't just toggle visibility. Animate with opacity, scale, and blur — the blur masks the imperfect transition between two distinct shapes, creating the illusion of a smooth morph.

The exact values: scale from 0.25 to 1, blur from 4px to 0px, opacity from 0 to 1. Without a motion library, use CSS transitions with 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);
}

Staggered enter

Don't animate an entire container. Break content into semantic chunks and stagger each with ~100ms delay. Combine opacity, translateY, and blur for the enter effect:

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

Surfaces

Concentric border-radius

When rounded elements are nested, the outer radius must equal the inner radius plus the padding between them: outer = inner + padding. Same radius on parent and child is the most common thing that makes interfaces feel off.

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

Shadows over borders

For cards, buttons, and containers, prefer layered box-shadow over solid borders. Shadows adapt to any background via transparency — solid borders don't.

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

Typography

Three simple rules that make all the difference: text-wrap: balance on headings for even line breaks, text-wrap: pretty on paragraphs to avoid orphans, and font-variant-numeric: tabular-nums on any dynamically changing number to prevent layout shift.

Apply -webkit-font-smoothing: antialiased on the root for crisper text on macOS.

Performance

Only animate transform and opacity — these properties skip layout and paint, running on the GPU. Animating width, height, padding, or margin triggers all three rendering steps.

transform, opacityGPU: Animate freely
width, height, marginGPU: Avoid

Never use transition: all. Always specify the exact properties:

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

These recommendations are compiled into a skill file for Claude Code, Cursor, and similar tools. Works with Laravel Blade, Livewire, and any CSS project:

npx skills add matheuscarddoso/blade-ui-skill

Based on the principles of Emil Kowalski, Ibelick and Jakub Krehel.