Animation
Motion principles, duration tokens, easing set, and reduced-motion rules for Sandworm. Motion is restrained — every transition clarifies a state change, never announces itself.
Motion principles
Reach for motion to confirm “your input registered” (hover/focus feedback) or to ease a layout shift (accordion open, element entering on scroll) — not for decoration.
Sandworm motion does not bounce.
Only ease-out, ease-in, ease-in-out, and the Material standard cubic-bezier(0.4, 0, 0.2, 1) ship. No spring or overshoot easing exists anywhere in the codebase. The “calm density” feel comes from preferring the slower 500ms lift on signature hovers over the SaaS-typical snappy 200ms.
Duration tokens
| Token | Value | Use | Live demo |
|---|---|---|---|
| fast | 200ms | Hover/focus feedback | hover me |
| base | 300ms | Standard transitions | hover me |
| slow | 500ms | Signature feature lifts, GradientButton | hover me |
| dramatic | 700ms | Rare — deliberate reveals | — |
| legato | 2000ms | One-off ambient loops only | — |
Named keyframe animations layer additional durations on top of these tokens: 0.2s (accordion), 0.4s (logo slide), 0.8s (fade-in), 1s (logo reveal), and 2–10s looping ambient loops (float, twinkle, glow-pulse, shimmer).
Easing set
| Easing | Value | When to use |
|---|---|---|
| ease-out | ease-out | Entrances — element decelerates into view. Accordion, fade-in entrance. |
| ease-in | ease-in | Exits — element accelerates out. Logo slide-out. |
| ease-in-out | ease-in-out | Default — symmetric ramp. GradientButton gradient pan, hover lifts, ambient loops. |
| material | cubic-bezier(0.4, 0, 0.2, 1) | Logo-fade-in, flipbook/storybook card lifts, shimmer. The only custom bezier. |
No spring or overshoot easing. cubic-bezier(0.34, 1.56, …) and similar bounce curves do not exist anywhere in the codebase. Don't add them — bounce breaks the restrained register. Sandworm motion glides; it never springs.
Shipped keyframes
glow
2s ease-in-out infinite — pulsing aura on a hovered or featured element. Ramps brightness(100%→150%), blur(0px→1px), and a box-shadow halo at the 50% mark, then back.
accordion-down / accordion-up
0.2s ease-out — animates height to/from var(--radix-accordion-content-height). Radix accordion content expand/collapse.
fade-in
0.8s ease-out forwards — opacity 0→1 with translateY(30px)→0. Scroll/load entrance for content blocks.
logo-fade-in
1s cubic-bezier(0.4, 0, 0.2, 1) forwards — opacity 0→0.8, scale 0.85→1, blur(12px)→0. The blur-in logo entrance.
logo-slide-out / logo-slide-in
Both 0.4s forwards (ease-in out, ease-out in) — opacity + translateY swap between rotating logos.
Ambient float loops
float (4s), float-reverse (5s), float-gentle (6s), illustration-float (8s) — gentle translateY + sub-degree rotation drift on illustrations. All ease-in-out infinite. Storybook/story surfaces only.
GradientButton gradient-slide
The signature CTA pattern. Not a keyframe — a moving background:
- Outer wrapper:
bg-gradient-to-rfrom sand-300 → red-400 → violet-600, sizedbg-[length:200%_200%], positionedbg-left. - On hover:
hover:bg-rightslides the oversized gradient +hover:scale-[1.02]. - All under
transition-all duration-500 ease-in-out— the slow token, no bounce.
Live demos
fast (200ms ease-out) — hover feedback
slow (500ms ease-in-out) — signature lift
material bezier (500ms) — card lift
Reduced motion
@media (prefers-reduced-motion: reduce) is non-optional for any animated surface. Ship the off-switch in the same change as the animation.
Recommended global baseline
@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;
}
}JS guard for animation loops
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReducedMotion) {
requestAnimationFrame(animationLoop);
}Do / Don't
Do
- ✓Run hover/focus feedback on
fast (200ms) - ✓Run feature lifts on
slow (500ms) - ✓Reach for
ease-in-outby default;ease-outfor entrances - ✓Pair every animated surface with a
prefers-reduced-motion: reducerule in the same change
Don't
- ✗Add spring or overshoot easing — none ships, and bounce breaks the restrained register
- ✗Use sub-
200msdurations on hover states — they read as snap, not glide (100msis legacy; don't extend it) - ✗Use motion for decoration — every transition must clarify a state change