Section 05 · Foundations
Motion.
Motion exists to clarify cause and effect. Four durations, four easings, that's it. If you need a fifth, you're animating something that should be instant.
Durations
--vp-dur-instant · 80mscolor shifts on hover · selection state
--vp-dur-fast · 150msdefault · hover, focus ring, button press
--vp-dur-base · 240msenter / exit · dropdown · tooltip · toast slide-in
--vp-dur-slow · 400mslarge surfaces · modal open · panel slide
--vp-dur-pulse · 1600mslive indicators only · "server connected"
Easings
--vp-ease-standardcubic-bezier(0.2, 0.8, 0.2, 1) default · enter + exit
--vp-ease-emphasisedcubic-bezier(0.16, 1, 0.3, 1) large enter · modal · drawer
--vp-ease-incubic-bezier(0.5, 0, 0.75, 0) exit · fades only
--vp-ease-outcubic-bezier(0.25, 1, 0.5, 1) enter · used rarely
The pulse
Reserved for genuinely live state. Server connection indicator. Active probe streaming. SSE connection. Never on a "Pro!" badge or a CTA — that's spam.
/* The system pulse · 1600ms loop */ @keyframes vp-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } .live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--vp-color-neon); box-shadow: 0 0 8px var(--vp-color-neon-glow); animation: vp-pulse var(--vp-dur-pulse) infinite; }
Where motion is forbidden
- Tables. Never animate row insertion. Logs and history scroll-jank when rows fade-in.
- Layout shifts. Never animate width or height of a container that holds text.
- Page transitions in the app. Click means navigate. The next view appears. We're not a slideshow.
- Loading shimmer on data-grids. Show a subtle 1-line "fetching · 1.2s" state instead. Shimmers are sugar.
prefers-reduced-motion
Respect it without compromise. Pulse stops. Toasts appear without slide. Modals fade only.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}