| name | framer-motion-landing |
| description | Framer Motion animation patterns for landing pages. Use when implementing scroll reveals, staggered grids, hover effects, or microinteractions. |
Framer Motion — Landing Page Patterns
Animation Variants (lib/animations.ts)
Centralized, reusable motion variants:
export const fadeInUp = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export const fadeInLeft = {
hidden: { opacity: 0, x: -24 },
visible: { opacity: 1, x: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export const fadeInRight = {
hidden: { opacity: 0, x: 24 },
visible: { opacity: 1, x: 0, transition: { duration: 0.5, ease: "easeOut" } },
};
export const scaleIn = {
hidden: { opacity: 0, scale: 0.95 },
visible: { opacity: 1, scale: 1, transition: { duration: 0.4, ease: "easeOut" } },
};
export const staggerContainer = {
hidden: {},
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.1 } },
};
export const staggerItem = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } },
};
Scroll-Triggered Reveals
Use whileInView instead of animate for sections below the fold:
<motion.div
variants={fadeInUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
>
<SectionContent />
</motion.div>
Rules:
viewport.once: true — animate only on first intersection
viewport.amount: 0.3 — trigger when 30% visible
- Hero uses
animate="visible" (above fold, no scroll trigger)
- All other sections use
whileInView
Staggered Grids
For feature cards, benefits, testimonials:
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
className="grid grid-cols-1 md:grid-cols-3 gap-6"
>
{items.map((item) => (
<motion.div key={item.id} variants={staggerItem}>
<Card>{item.content}</Card>
</motion.div>
))}
</motion.div>
Hover & Press Feedback
Cards
<motion.div
whileHover={{ y: -4, boxShadow: "0 8px 30px rgba(0,0,0,0.12)" }}
transition={{ duration: 0.2 }}
>
<Card />
</motion.div>
Buttons
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15 }}
>
Get Started
</motion.button>
Theme Toggle Animation
Smooth icon rotation between Sun/Moon:
<motion.div
animate={{ rotate: theme === "dark" ? 180 : 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
>
{theme === "dark" ? <Moon /> : <Sun />}
</motion.div>
Reduced Motion Support
ALL animations must respect user preference:
const shouldReduceMotion = typeof window !== "undefined"
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
: false;
const variants = shouldReduceMotion
? { hidden: {}, visible: {} }
: fadeInUp;
Or use Framer Motion's built-in hook:
import { useReducedMotion } from "framer-motion";
const prefersReducedMotion = useReducedMotion();
Performance Rules
- Never animate
width, height, or top/left — use transform and opacity only
- Use
will-change: transform sparingly and only on actively animated elements
whileInView is more performant than scroll listeners + manual state
- Keep animation durations under 0.6s for UI elements
- Use
layout prop only when needed (layout animations are expensive)
Pattern Per Section
| Section | Animation |
|---|
| Hero | fadeInUp on headline, stagger on CTA group, animate (not whileInView) |
| Features | staggerContainer + staggerItem on grid, whileInView |
| Benefits | fadeInLeft/fadeInRight alternating, whileInView |
| Social Proof | scaleIn on stat numbers, fadeInUp on testimonials |
| Final CTA | fadeInUp on content, scaleIn on form |
| Navbar | Fade in on mount, scroll-based background opacity |