Animaciones
base.css incluye un sistema de animaciones completo. Todos los @keyframes están centralizados allí y los archivos de @headless-primitives/styles los reutilizan sin duplicarlos.
Variables de timing
Las duraciones y curvas están expuestas como CSS custom properties con fallback incorporado:
| Variable | Valor por defecto | Uso |
|---|---|---|
--hp-duration-fast | 100ms | Tooltip |
--hp-duration | 150ms | Popover, close animations |
--hp-duration-slow | 200ms | Dialog, backdrop, toast entrada |
--hp-ease | ease | Animaciones de salida y fade |
--hp-ease-out | cubic-bezier(0.16, 1, 0.3, 1) | Entradas (spring suave, estilo Radix) |
Puedes sobrescribir cualquier variable en :root:
:root {
--hp-duration-slow: 300ms; /* entradas más lentas */
--hp-ease-out: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring más pronunciado */
}Animaciones por componente
Overlays: Dialog
La entrada usa scale(0.95 → 1) + fade, preservando el translate(-50%, -50%) del centrado estructural. La salida es la inversa.
/* Keyframes definidos en base.css */
@keyframes hp-dialog-in {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes hp-dialog-out {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.96);
}
}El backdrop hace fade independiente:
@keyframes hp-backdrop-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes hp-backdrop-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}Overlays: Drawer
El drawer usa transform para deslizarse desde un borde del viewport. La dirección del slide es automática según el atributo data-position que hp-drawer sincroniza en el content.
No usa @keyframes propios — la animación es una transición CSS sobre transform + opacity + visibility. Esto permite que el slide respete exactamente la curva --hp-ease-out sin necesidad de keyframes por cada posición:
/* Transición en base.css — transform incluido solo para el drawer */
[data-hp-drawer-content] {
transition:
opacity var(--hp-duration) ease,
visibility 0s linear var(--hp-duration),
transform var(--hp-duration) cubic-bezier(0.16, 1, 0.3, 1);
}
/* Estado inicial (fuera del viewport) por posición */
[data-hp-drawer-content] {
transform: translateX(-100%);
} /* left */
[data-hp-drawer-content][data-position="right"] {
transform: translateX(100%);
} /* right */
[data-hp-drawer-content][data-position="top"] {
transform: translateY(-100%);
} /* top */
[data-hp-drawer-content][data-position="bottom"] {
transform: translateY(100%);
} /* bottom*/
/* Estado abierto — sin transform */
[data-hp-drawer-content][data-state="open"] {
transform: translate(0, 0);
}El backdrop comparte los keyframes hp-backdrop-in / hp-backdrop-out con el dialog.
Para personalizar la velocidad del slide independientemente del resto de overlays:
:root {
--hp-duration: 250ms; /* afecta todos los overlays */
}
/* O solo el drawer */
hp-drawer-content {
transition-duration: 350ms;
}Overlays: Popover
Slide vertical + scale leve. Salida es más rápida que la entrada (patrón estándar):
@keyframes hp-overlay-in {
from {
opacity: 0;
transform: translateY(-6px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes hp-overlay-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-4px) scale(0.97);
}
}Tooltip
Igual que el popover pero más corto y preserva el translateX(-50%) del centrado horizontal:
@keyframes hp-tooltip-in {
from {
opacity: 0;
transform: translateX(-50%) translateY(-4px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}Toast
El slide adapta su dirección según la posición del contenedor usando una custom property heredable:
/* El contenedor define la dirección de slide */
hp-toast-container {
--hp-toast-slide: 110%;
} /* derecha (default) */
hp-toast-container[data-position*="left"] {
--hp-toast-slide: -110%;
} /* izquierda */
hp-toast-container[data-position*="center"] {
--hp-toast-slide: 0px;
} /* centrado → scale */
@keyframes hp-toast-in {
from {
opacity: 0;
transform: translateX(var(--hp-toast-slide, 110%));
}
to {
opacity: 1;
transform: translateX(0);
}
}Los toasts centrados usan animación diferente (scale en lugar de slide):
@keyframes hp-toast-in-center {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}Paneles (Accordion, Tabs, Collapsible)
Los paneles se muestran/ocultan con display: none. Para animar la entrada se usa @starting-style, que captura el estado inicial justo después de que display: none es removido:
[data-hp-panel][data-state="open"],
[data-hp-panel][data-state="selected"] {
transition: opacity var(--hp-duration) var(--hp-ease);
}
@starting-style {
[data-hp-panel][data-state="open"],
[data-hp-panel][data-state="selected"] {
opacity: 0;
}
}
@starting-stylerequiere Chrome 117+, Safari 17.5+, Firefox 129+. En navegadores sin soporte los paneles simplemente aparecen sin animación.
Visibilidad con transición suave
Los overlays usan una técnica de visibility con delay para que la animación de salida pueda completarse antes de que el elemento desaparezca del árbol de accesibilidad:
/* Base: cuando cierra, opacity primero, visibility después */
[data-hp-overlay-content] {
transition:
opacity var(--hp-duration) var(--hp-ease),
visibility 0s linear var(--hp-duration); /* delay = duración de la animación */
}
/* Al abrir: visibility es inmediata (sin delay) */
[data-hp-overlay-content][data-state="open"] {
transition:
opacity var(--hp-duration) var(--hp-ease),
visibility 0s; /* sin delay */
}Desactivar animaciones
Respetar prefers-reduced-motion
Añade esto a tu CSS global para usuarios que prefieren menos movimiento:
@media (prefers-reduced-motion: reduce) {
:root {
--hp-duration-fast: 0ms;
--hp-duration: 0ms;
--hp-duration-slow: 0ms;
}
}Al poner las duraciones a 0ms, las transiciones y animaciones son instantáneas sin eliminar el estado final correcto.
Desactivar completamente
:root {
--hp-duration-fast: 0ms;
--hp-duration: 0ms;
--hp-duration-slow: 0ms;
}Personalizar una animación específica
Dado que los @keyframes tienen nombres globales (hp-dialog-in, hp-overlay-in, etc.), puedes reemplazar la animación de un componente puntual sobreescribiendo solo la regla de aplicación:
/* Reemplazar la entrada del dialog con un flip vertical */
@keyframes my-dialog-in {
from {
opacity: 0;
transform: translate(-50%, -50%) rotateX(-8deg) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) rotateX(0deg) scale(1);
}
}
[data-hp-overlay-content][role="dialog"][data-state="open"] {
animation: my-dialog-in 250ms cubic-bezier(0.16, 1, 0.3, 1) both;
}/* Hacer que el toast entre desde abajo */
@keyframes my-toast-in {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
hp-toast {
animation: my-toast-in var(--hp-duration-slow) var(--hp-ease-out) both;
}