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.
Same-day delivery in Guatemala City for orders before noon.
Two to four business days anywhere else in the country.
Free over Q500.
Instalación
$ pnpm dlx shadcn@latest add @ssntl/motion-kitUso
/* 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).
## 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.Código
/* 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%;
}
}