Sandworm

Animation

ready

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

TokenValueUseLive demo
fast200msHover/focus feedbackhover me
base300msStandard transitionshover me
slow500msSignature feature lifts, GradientButtonhover me
dramatic700msRare — deliberate reveals
legato2000msOne-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

EasingValueWhen to use
ease-outease-outEntrances — element decelerates into view. Accordion, fade-in entrance.
ease-inease-inExits — element accelerates out. Logo slide-out.
ease-in-outease-in-outDefault — symmetric ramp. GradientButton gradient pan, hover lifts, ambient loops.
materialcubic-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-r from sand-300 → red-400 → violet-600, sized bg-[length:200%_200%], positioned bg-left.
  • On hover: hover:bg-right slides 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

card lift · cubic-bezier(0.4, 0, 0.2, 1)

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-out by default; ease-out for entrances
  • Pair every animated surface with a prefers-reduced-motion: reduce rule in the same change

Don't

  • Add spring or overshoot easing — none ships, and bounce breaks the restrained register
  • Use sub-200ms durations on hover states — they read as snap, not glide (100ms is legacy; don't extend it)
  • Use motion for decoration — every transition must clarify a state change