Saltar al contenido
ssntl/ui

Motion kit

Animaciones pequeñas sobre los tokens de movimiento: un shake de error, una altura plegable, hijos que continúan y el deslizamiento de un sheet.

Instalación

$ pnpm dlx shadcn@latest add @ssntl/motion-kit

Uso

css
/* app/globals.css */
@import "../styles/ssntl/motion-kit.css";

<input class={invalido ? "shake" : ""} aria-invalid={invalido} />
<div class="collapsible" data-state="open"><div class="follow-through">…</div></div>

Reglas

Lo que este componente espera de ti, tal como se instala en styles/ssntl/motion-kit.md junto al código. El archivo completo agrega cuándo usarlo, teclado, los fundamentos de UI que aplica y cada regla compartida (en inglés).

styles/ssntl/motion-kit.md
## Reglas de Motion kit

- `shake`: ±4px (spacing-1) en `--duration-menu`; acompáñalo de `aria-invalid` y un mensaje, porque con movimiento reducido no corre.
- `collapsible` anima la altura de un desplegable de Radix; `follow-through` hace entrar a sus hijos a 40ms entre sí, después del contenedor.
- `sheet-in` y `sheet-out` deslizan un panel desde el borde inferior, más rápido al salir que al entrar.
- Cada animación se apaga con movimiento reducido; ningún estado vive solo en el movimiento.
Abrir el archivo de reglas completo (se abre en otra pestaña)

Código

styles/ssntl/motion-kit.css
/* Motion kit: small, purposeful animations built on the motion tokens.
   Every one turns off under reduced motion, and none carries state on its
   own: a color, an icon or a word stays behind when it doesn't run.

     shake               a noticeable but gentle error nudge
     collapsible         height for a disclosure (Radix --*-content-height)
     follow-through      children enter just after their container */

/* Noticeable errors: ±4px (spacing-1) over --duration-menu. Big enough to
   catch the eye, too small to jar; the message explains the error. */
@utility shake {
  @media (prefers-reduced-motion: no-preference) {
    animation: shake var(--duration-menu) var(--ease-out);
  }
}

@keyframes shake {
  20% {
    translate: calc(var(--spacing) * -1) 0;
  }
  45% {
    translate: var(--spacing) 0;
  }
  70% {
    translate: calc(var(--spacing) * -0.5) 0;
  }
}

/* Follow through & overlap: the container opens first, then its rows lift
   in 40ms apart, so the motion settles instead of landing at once. */
@utility collapsible {
  overflow: hidden;
  &[data-state="open"] {
    animation: collapse-open var(--duration-menu) var(--ease-out);
  }
  &[data-state="closed"] {
    animation: collapse-close var(--duration-fast) var(--ease-out);
  }
  @media (prefers-reduced-motion: reduce) {
    animation: none !important;
  }
}

/* Put follow-through on the element inside an open disclosure (Radix sets
   data-state="open" on its parent). */
@utility follow-through {
  --follow-delay: calc(var(--duration-fast) * 0.66);
  --follow-step: 40ms;
  @media (prefers-reduced-motion: no-preference) {
    [data-state="open"] > & > * {
      animation: follow-in var(--duration-menu) var(--ease-out) both;
      animation-delay: var(--follow-delay);
    }
    [data-state="open"] > & > :nth-child(2) {
      animation-delay: calc(var(--follow-delay) + var(--follow-step));
    }
    [data-state="open"] > & > :nth-child(3) {
      animation-delay: calc(var(--follow-delay) + var(--follow-step) * 2);
    }
    [data-state="open"] > & > :nth-child(n + 4) {
      animation-delay: calc(var(--follow-delay) + var(--follow-step) * 3);
    }
  }
}

@keyframes collapse-open {
  from {
    height: 0;
  }
  to {
    height: var(--radix-accordion-content-height, auto);
  }
}

@keyframes collapse-close {
  from {
    height: var(--radix-accordion-content-height, auto);
  }
  to {
    height: 0;
  }
}

@keyframes follow-in {
  from {
    opacity: 0;
    translate: 0 calc(var(--es-lift) * -0.5);
  }
}

/* Sheets slide up from the bottom edge and leave faster than they came. */
@utility sheet-in {
  animation: sheet-in var(--duration-panel) var(--ease-out);
  @media (prefers-reduced-motion: reduce) {
    animation-name: fade-in;
  }
}

@utility sheet-out {
  animation: sheet-out var(--duration-menu) var(--ease-out) forwards;
  @media (prefers-reduced-motion: reduce) {
    animation-name: fade-out;
  }
}

@keyframes sheet-in {
  from {
    translate: 0 100%;
  }
}

@keyframes sheet-out {
  to {
    translate: 0 100%;
  }
}