Skip to content

Motion

Principles

  1. Purposeful — Every animation communicates meaning: something appeared, changed, or needs attention.
  2. Fast — No animation exceeds 400ms. Hero entrance sequences are the only exception.
  3. Respectful — All animations are disabled when prefers-reduced-motion: reduce is active.

Duration Scale

NameValueUsage
Instant0msState changes with no perceptible transition (focus rings)
Fast150msHover states, button press
Base200msFade in/out, color transitions
Moderate300msModal open/close, dropdown
Slow400msPage section entrance on scroll
Hero600ms–800msAbove-the-fold staggered entrance

Easing Presets

lib/motion.ts
export const ease = {
// Standard ease-out for entrances
out: [0.0, 0.0, 0.2, 1],
// Ease-in for exits
in: [0.4, 0.0, 1, 1],
// Smooth for transforms
inOut: [0.4, 0.0, 0.2, 1],
// Spring-like — for elements that 'settle'
spring: { type: 'spring', stiffness: 300, damping: 30 },
} as const;

Framer Motion Variants

Fade Up (section entrance)

const fadeUp = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0, 0, 0.2, 1] } },
};
<motion.div variants={fadeUp} initial="hidden" whileInView="visible" viewport={{ once: true }}>
</motion.div>

Stagger Children (card grids)

const container = {
hidden: {},
visible: { transition: { staggerChildren: 0.08 } },
};
const item = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3 } },
};
<motion.div variants={container} initial="hidden" whileInView="visible" viewport={{ once: true }}>
{cards.map((c) => (
<motion.div key={c.id} variants={item}>{/* card */}</motion.div>
))}
</motion.div>

Button Press

<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15 }}
>
</motion.button>

Ticker / Marquee (Press logos bar)

<motion.div
animate={{ x: ['0%', '-50%'] }}
transition={{ duration: 30, ease: 'linear', repeat: Infinity }}
>
{/* duplicated logo set */}
</motion.div>

Reduced Motion

Wrap all Framer Motion components with a reduced-motion check:

lib/useReducedMotion.ts
import { useReducedMotion } from 'framer-motion';
export function useMotionVariants(variants: Variants) {
const reduced = useReducedMotion();
if (reduced) {
return { hidden: {}, visible: {} };
}
return variants;
}

Alternatively, set transition.duration: 0 when reduced is true.

CSS Transitions (Tailwind)

For non-Framer-Motion hover states, use Tailwind transition utilities:

ClassUsage
transition-colorsColor and background changes on hover
transition-opacityFade in/out
transition-transformScale/translate on hover
duration-150Fast transitions (buttons)
duration-200Standard transitions
ease-outDefault easing
<a class="transition-colors duration-150 ease-out hover:text-brand-red">
Link text
</a>