Interface details
Los detalles que hacen que una interfaz se sienta bien: sombras en lugar de bordes, contorno en imágenes, íconos que se cruzan y entradas por partes.

Color sphereQ 1,250.00
Instalación
$ pnpm dlx shadcn@latest add @ssntl/detailsUso
/* app/globals.css */
@import "../styles/ssntl/details.css";
/* En tu JSX */
<div class="border-shadow hover:border-shadow-strong">…</div>
<img class="image-outline" … />
<span class="icon-swap" data-swapped={copied}><Copy /><Check /></span>
<p class="animate-enter stagger-1">…</p>- border-shadow: tres capas transparentes en lugar de un borde; en oscuro, un borde de 1px al 8%.
- image-outline: 1px negro (blanco en oscuro) al 10%, hacia adentro. Nunca un gris teñido.
- icon-swap: los dos íconos en el DOM, cruzados con opacidad, escala 0.25 → 1 y desenfoque 4px → 0.
- animate-enter + stagger-{n}: entradas por partes a 100ms; enter-words baja el paso a 80ms para títulos.
- Todo se apaga o se simplifica con movimiento reducido. La guía completa está en /es/docs/details.
Código
/* Interface details: the small things that compound into an interface that
feels right. After Jakub Krehel's "Details that make interfaces feel better".
border-shadow depth without a border (3 layers, 1px in dark)
border-shadow-strong the same, a step darker (hover, selected)
image-outline 1px pure black/white at 10% inside images
icon-swap cross-fade two icons: opacity, scale, blur
animate-enter, stagger-{n} split-and-stagger entrance, 100ms apart
enter-words 80ms steps, for a title split into words
Text wrapping, font smoothing and tabular numbers are Tailwind built-ins:
text-balance, text-pretty, antialiased, tabular-nums. */
/* Shadows instead of borders: transparent layers adapt to any background a
solid border was not designed for. In dark the stack reads as a 1px edge. */
@utility border-shadow {
box-shadow:
0 0 0 1px rgb(0 0 0 / calc(0.06 * var(--border-shadow-step, 1))),
0 1px 2px -1px rgb(0 0 0 / calc(0.06 * var(--border-shadow-step, 1))),
0 2px 4px 0 rgb(0 0 0 / calc(0.04 * var(--border-shadow-step, 1)));
transition-property: box-shadow;
transition-duration: var(--duration-fast);
transition-timing-function: var(--ease-out);
@variant dark {
box-shadow: 0 0 0 1px
rgb(255 255 255 / calc(0.08 * var(--border-shadow-step, 1)));
}
}
@utility border-shadow-strong {
--border-shadow-step: 1.4;
}
/* A consistent edge on images, whatever is behind them. Pure black or white,
never a tinted neutral: a tint picks up the surface and reads as dirt. */
@utility image-outline {
outline: 1px solid oklch(0 0 0 / 0.1);
outline-offset: -1px;
@variant dark {
outline-color: oklch(1 0 0 / 0.1);
}
}
/* Contextual icons: both stay in the DOM and cross-fade, so the swap has an
enter and an exit. Set data-swapped on the wrapper:
<span class="icon-swap" data-swapped={copied}>
<Copy /> <Check />
</span>
With reduced motion only the opacity fade remains. */
@utility icon-swap {
display: inline-grid;
place-items: center;
& > * {
grid-area: 1 / 1;
transition-property: opacity, scale, filter;
transition-duration: var(--duration-pop);
transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
&:not([data-swapped="true"]) > :last-child,
&[data-swapped="true"] > :first-child {
opacity: 0;
scale: 0.25;
filter: blur(4px);
}
@media (prefers-reduced-motion: reduce) {
& > * {
scale: 1 !important;
filter: none !important;
}
}
}
/* Split and stagger: animate the title, the description and the actions as
separate chunks, 100ms apart, instead of one block. A keyframe runs once,
which is right for a staged entrance; interactions use transitions. */
@utility animate-enter {
@media (prefers-reduced-motion: no-preference) {
animation: enter 700ms var(--ease-out) both;
animation-delay: calc(var(--enter-delay, 100ms) * var(--stagger, 0));
}
}
@utility stagger-* {
--stagger: --value(integer);
}
@utility enter-words {
--enter-delay: 80ms;
}
@keyframes enter {
from {
opacity: 0;
translate: 0 var(--es-lift, 8px);
filter: blur(8px);
}
}