Building Interactive Landing Page: Complete Framer Motion Project
This final article brings together everything from the series: motion components, variants, springs, gestures, and exit animations applied to a real-world landing page. You'll build a polished, interactive landing with an animated hero section, staggered feature cards, hover effects, and a mobile-responsive testimonial carousel. This is production-ready code you can adapt for your projects.
I've shipped landing pages using this exact pattern for SaaS startups and design agencies. The combination of entrance animations, gesture feedback, and micro-interactions creates the premium feel that converts visitors to customers.
Project Overview
The landing page includes:
- Hero section: Large animated title, subtitle, and CTA button.
- Features grid: Cards with staggered entrance and hover animations.
- Testimonials carousel: Draggable, snappable testimonial cards.
- Footer: Simple footer with delayed entrance.
All animations use principles from previous articles: spring transitions for interactive feel, tween for sequential timing, variants for orchestration, and gestures for feedback.
Hero Section Animation
The hero animates on mount with a staggered, cascading effect:
import { motion } from 'framer-motion';
const heroVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.2,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } }
};
export function HeroSection() {
return (
<motion.section
variants={heroVariants}
initial="hidden"
animate="visible"
style={{
minHeight: '500px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '60px 20px',
textAlign: 'center',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}
>
<motion.h1
variants={itemVariants}
style={{
fontSize: '48px',
fontWeight: 'bold',
color: '#fff',
margin: '0 0 20px 0'
}}
>
Build Amazing Animations
</motion.h1>
<motion.p
variants={itemVariants}
style={{
fontSize: '20px',
color: '#f0f0f0',
maxWidth: '600px',
margin: '0 0 30px 0'
}}
>
Create polished, responsive animations with Framer Motion and React.
</motion.p>
<motion.button
variants={itemVariants}
whileHover={{ scale: 1.08 }}
whileTap={{ scale: 0.95 }}
style={{
padding: '14px 32px',
backgroundColor: '#fff',
color: '#667eea',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
}}
>
Get Started
</motion.button>
</motion.section>
);
}
The hero container uses variants with staggerChildren: 0.2 so each child (h1, p, button) animates 0.2 seconds after the previous one. The delayChildren: 0.3 adds a 0.3-second delay before any animation starts, creating breathing room. Each item uses itemVariants to fade in and slide up over 0.6 seconds.
Features Grid with Hover Effects
The features grid displays cards with entrance stagger and individual hover feedback:
import { motion } from 'framer-motion';
const featuresVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1
}
}
};
const featureCardVariants = {
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 0.5 }
}
};
export function FeaturesSection() {
const features = [
{ icon: '⚡', title: 'Fast', description: 'GPU-accelerated animations at 60fps' },
{ icon: '🎨', title: 'Customizable', description: 'Declarative props for full control' },
{ icon: '📱', title: 'Responsive', description: 'Works seamlessly on all devices' },
{ icon: '🔄', title: 'Flexible', description: 'Gestures, drag, and variants included' },
{ icon: '♿', title: 'Accessible', description: 'Keyboard and screen reader support' },
{ icon: '🚀', title: 'Production-Ready', description: 'Used by Stripe, Slack, OpenAI' }
];
return (
<motion.section
variants={featuresVariants}
initial="hidden"
whileInView="visible"
style={{
padding: '80px 20px',
maxWidth: '1200px',
margin: '0 auto'
}}
>
<h2
style={{
fontSize: '36px',
fontWeight: 'bold',
textAlign: 'center',
marginBottom: '60px'
}}
>
Why Choose Framer Motion?
</h2>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '30px'
}}
>
{features.map((feature, i) => (
<motion.div
key={i}
variants={featureCardVariants}
whileHover={{
y: -12,
boxShadow: '0 20px 40px rgba(0,0,0,0.15)'
}}
transition={{
type: 'spring',
stiffness: 300,
damping: 20
}}
style={{
padding: '30px',
backgroundColor: '#fff',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
cursor: 'pointer',
transition: 'box-shadow 0.3s ease'
}}
>
<div
style={{
fontSize: '40px',
marginBottom: '15px'
}}
>
{feature.icon}
</div>
<h3
style={{
fontSize: '20px',
fontWeight: 'bold',
marginBottom: '10px'
}}
>
{feature.title}
</h3>
<p
style={{
color: '#666',
lineHeight: '1.6'
}}
>
{feature.description}
</p>
</motion.div>
))}
</div>
</motion.section>
);
}
The whileInView="visible" prop triggers animations when the section scrolls into view. Each card animates with staggerChildren: 0.1, so they cascade down. On hover, cards lift up (y: -12) and a shadow deepens using a spring transition for snappy feedback.
Testimonials Carousel with Drag
A draggable testimonial carousel with snap points:
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
export function TestimonialsSection() {
const [index, setIndex] = React.useState(0);
const testimonials = [
{ author: 'Alice Chen', role: 'Founder, Design Startup', quote: 'Framer Motion transformed how we build animations. We shipped 50% faster.' },
{ author: 'Bob Rodriguez', role: 'Lead Developer, SaaS', quote: 'The gesture integration is seamless. Our users love the interactive feel.' },
{ author: 'Carol White', role: 'UX Designer, Agency', quote: 'Finally, animations that feel natural. Spring physics changed everything.' }
];
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0 }
};
return (
<motion.section
variants={containerVariants}
initial="hidden"
whileInView="visible"
style={{
padding: '80px 20px',
backgroundColor: '#f9f9f9'
}}
>
<motion.h2
variants={itemVariants}
style={{
fontSize: '36px',
fontWeight: 'bold',
textAlign: 'center',
marginBottom: '60px'
}}
>
What Our Users Say
</motion.h2>
<div
style={{
maxWidth: '800px',
margin: '0 auto'
}}
>
<AnimatePresence mode="wait">
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4 }}
drag="x"
dragElastic={0.2}
dragConstraints={{ left: -100, right: 100 }}
onDragEnd={(e, info) => {
if (info.offset.x < -50) {
setIndex((prev) => (prev + 1) % testimonials.length);
} else if (info.offset.x > 50) {
setIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
}
}}
style={{
padding: '40px',
backgroundColor: '#fff',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
cursor: 'grab',
userSelect: 'none',
touchAction: 'none'
}}
>
<p
style={{
fontSize: '18px',
fontStyle: 'italic',
color: '#333',
marginBottom: '20px',
lineHeight: '1.6'
}}
>
"{testimonials[index].quote}"
</p>
<p
style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#667eea'
}}
>
{testimonials[index].author}
</p>
<p
style={{
fontSize: '14px',
color: '#999'
}}
>
{testimonials[index].role}
</p>
</motion.div>
</AnimatePresence>
<div
style={{
display: 'flex',
justifyContent: 'center',
gap: '10px',
marginTop: '30px'
}}
>
{testimonials.map((_, i) => (
<motion.button
key={i}
onClick={() => setIndex(i)}
animate={{
backgroundColor: i === index ? '#667eea' : '#ddd',
scale: i === index ? 1.1 : 1
}}
whileHover={{ scale: 1.2 }}
style={{
width: '12px',
height: '12px',
borderRadius: '50%',
border: 'none',
cursor: 'pointer'
}}
/>
))}
</div>
</div>
</motion.section>
);
}
The testimonial card is draggable horizontally. Drag right to go to the previous testimonial, left for the next. The card exits and enters with a smooth fade-and-slide using AnimatePresence with mode="wait". Below, dot indicators respond to the current slide, animating color and scale on selection.
Complete Landing Page Component
Combine all sections:
import { motion } from 'framer-motion';
import React from 'react';
export function LandingPage() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<HeroSection />
<FeaturesSection />
<TestimonialsSection />
<motion.footer
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
style={{
padding: '40px 20px',
backgroundColor: '#333',
color: '#fff',
textAlign: 'center'
}}
>
<p>Built with Framer Motion and React. © 2026 Your Company.</p>
</motion.footer>
</motion.div>
);
}
The page combines entrance animations (hero cascading), interactive elements (feature card hovers), drag interaction (testimonial carousel), and exit animations (page fade). Every animation is intentional and enhances perceived performance.
Key Patterns Used
| Pattern | Purpose | Where Used |
|---|---|---|
| Staggered entrance | Orchestrate multiple elements sequentially | Hero, features grid |
| Spring transitions | Responsive, playful feedback | Feature card hovers, dot indicators |
| Variants | Reusable animation shapes | All sections |
| Drag with snap | Interactive navigation | Testimonial carousel |
| AnimatePresence | Smooth exits during transitions | Testimonial carousel |
whileInView | Trigger animations on scroll | Features and testimonials sections |
Performance Considerations for This Landing
- Hero: 3–4 animated elements with simple opacity/y. No impact.
- Features grid: 6 cards with
scaleXand shadow on hover. GPU-accelerated, no reflows. - Testimonials: Single card animated, drag-based. Lightweight.
- Overall: Maintains 60fps on modern devices and acceptable 30fps+ on older devices.
If performance drops, reduce the number of feature cards, simplify transitions (duration: 0.3 instead of 0.6), or disable animations on low-end devices via feature detection.
Next Steps
- Adapt the layout and colors to match your brand.
- Add more sections (team, pricing, FAQ) with the same animation patterns.
- Integrate with a backend for real testimonial data.
- Test on mobile devices and optimize touch interactions.
- Deploy to production and monitor performance with Web Vitals.
Key Takeaways
- Combine entrance animations, hover gestures, and drag interactions for a polished landing page.
- Use variants for orchestration and spring transitions for interactive feedback.
- Test animations on real devices to ensure 60fps performance.
- Apply the same patterns (stagger, gestures, variants) across all sections for consistency.
Frequently Asked Questions
How do I adapt this landing page to my product?
Replace the copy (title, features, testimonials), colors (gradient, accents), and icons. Keep the animation structure intact; the patterns are universal.
Can I add more sections (pricing, FAQ)?
Yes. Use the same variant pattern: container with staggerChildren, items with itemVariants. Wrap in whileInView to animate on scroll.
How do I make the landing page mobile-responsive?
Use CSS media queries to adjust font sizes, grid columns, and padding. Animations scale automatically; Framer Motion doesn't require mobile-specific changes.
Can I export this as a template?
Yes. Refactor into a reusable component library with variants as props, then publish to npm or use as a starting point for new projects.