NewAppLander — App landing pages in 60s$69$39
The Swift Kit logoThe Swift Kit
Tutorial

Framer Motion Animations for App Landing Pages: A Developer's Guide

Well-crafted animations transform a static app landing page into an engaging experience. This guide covers the specific Framer Motion patterns that work best for app websites — scroll reveals, staggered grids, hover effects, and device mockup transitions — with copy-paste code for each.

Ahmed GaganAhmed Gagan
14 min read

TL;DR

Framer Motion is the best animation library for React-based app landing pages. The key patterns: whileInView for scroll reveals, staggerChildren for feature grids, whileHover for card interactions, and AnimatePresence for screenshot carousels. Always respect prefers-reduced-motion. AppLander includes all of these animations pre-configured.

A landing page without animations feels static and lifeless. A landing page with too many animations feels like a carnival. The sweet spot — subtle, purposeful motion that guides the eye and creates a sense of polish — is what separates amateur app websites from professional ones.

Framer Motion is the library that makes this easy in React. Its declarative API lets you animate anything with simple props, and its viewport detection handles scroll-triggered animations without manual scroll listeners. In this guide, I will show you the exact animation patterns that work best for app landing pages, with code you can copy directly into your Next.js project.

How Do You Set Up Framer Motion in Next.js?

Install Framer Motion in your Next.js project:

npm install framer-motion

Important: Framer Motion components are client components in Next.js App Router. Any component that uses Framer Motion needs the "use client" directive at the top of the file. This is fine for landing page sections — the trade-off between server rendering and animation capability is worth it.

'use client'
import { motion } from 'framer-motion'

export function AnimatedSection() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      Content appears with a fade-up animation on mount.
    </motion.div>
  )
}

The three core props: initial (the starting state), animate (the final state), and transition (how to get there). This pattern covers 80% of landing page animations.

How Do You Animate Elements on Scroll?

The most common animation on app landing pages is elements appearing as the user scrolls down. Framer Motion makes this trivial with the whileInView prop:

'use client'
import { motion } from 'framer-motion'

export function ScrollReveal({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 30 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: 0.6, ease: 'easeOut' }}
    >
      {children}
    </motion.div>
  )
}

Key details:

  • whileInView — Triggers the animation when the element enters the viewport. No scroll listeners, no IntersectionObserver setup — Framer Motion handles it.
  • viewport: { once: true } — The animation only plays once. Without this, elements re-animate every time they scroll in and out of view, which is disorienting.
  • margin: '-100px' — Triggers the animation 100px before the element enters the viewport. This makes the reveal feel smoother because the animation is already in progress when the element becomes visible.
  • ease: 'easeOut' — The animation decelerates at the end. This feels natural because real-world objects slow down as they come to rest.

Wrap any section of your landing page with this component for a clean scroll-reveal effect.

How Do You Create Staggered Feature Card Animations?

A feature grid where cards appear one after another (staggered) creates a dynamic, engaging reveal. The pattern uses staggerChildren on the parent container:

'use client'
import { motion } from 'framer-motion'

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,  // 100ms between each child
      delayChildren: 0.2,     // Wait 200ms before starting
    },
  },
}

const cardVariants = {
  hidden: { opacity: 0, y: 24 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4, ease: 'easeOut' },
  },
}

export function FeatureGrid({ features }: { features: Feature[] }) {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true, margin: '-50px' }}
      className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"
    >
      {features.map((feature) => (
        <motion.div
          key={feature.title}
          variants={cardVariants}
          className="rounded-2xl border border-white/10 bg-white/[0.02] p-6"
        >
          <h3 className="text-lg font-semibold">{feature.title}</h3>
          <p className="mt-2 text-sm text-white/50">{feature.description}</p>
        </motion.div>
      ))}
    </motion.div>
  )
}

The magic is in the variants system. When the container transitions from "hidden" to "visible," it automatically propagates that transition to all children. Each child waits its turn based on staggerChildren. Six cards with 100ms stagger means the last card appears 500ms after the first — a visually pleasing cascade effect.

How Do You Add Hover Effects to Cards?

Interactive hover effects make your feature cards feel alive. Framer Motion's whileHover and whileTap props handle this:

<motion.div
  whileHover={{
    y: -4,
    transition: { duration: 0.2 },
  }}
  whileTap={{ scale: 0.98 }}
  className="cursor-pointer rounded-2xl border border-white/10
    bg-white/[0.02] p-6 transition-colors duration-300
    hover:border-white/20 hover:bg-white/[0.05]"
>
  {/* Card content */}
</motion.div>

Notice the hybrid approach: Framer Motion handles the y (vertical shift) and scale animations because these need JavaScript for smooth performance. CSS handles the border-color and background-color transitions because these are GPU-accelerated in CSS. Using both together gives you the best performance.

How Do You Animate a Screenshot Carousel?

If your landing page has a screenshot gallery, animating the transitions between screenshots creates a polished, app-like experience:

'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'

const screenshots = [
  { src: '/screenshots/home.png', alt: 'Home screen' },
  { src: '/screenshots/stats.png', alt: 'Statistics' },
  { src: '/screenshots/settings.png', alt: 'Settings' },
]

export function ScreenshotCarousel() {
  const [current, setCurrent] = useState(0)

  return (
    <div className="relative">
      <AnimatePresence mode="wait">
        <motion.img
          key={screenshots[current].src}
          src={screenshots[current].src}
          alt={screenshots[current].alt}
          initial={{ opacity: 0, x: 50 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: -50 }}
          transition={{ duration: 0.3 }}
          className="rounded-2xl"
        />
      </AnimatePresence>

      <div className="mt-6 flex justify-center gap-2">
        {screenshots.map((_, i) => (
          <button
            key={i}
            onClick={() => setCurrent(i)}
            className={`h-2 rounded-full transition-all duration-300 ${
              i === current
                ? 'w-8 bg-accent'
                : 'w-2 bg-white/20 hover:bg-white/40'
            }`}
          />
        ))}
      </div>
    </div>
  )
}

AnimatePresence is the key component here. It detects when a child leaves the DOM and plays its exit animation before removing it. The mode="wait" prop ensures the exiting screenshot finishes its animation before the entering one starts, preventing visual overlap.

How Do You Animate Numbers and Statistics?

Animated counters for download counts, ratings, or user numbers add visual interest to your social proof section:

'use client'
import { useEffect, useRef, useState } from 'react'
import { useInView } from 'framer-motion'

function AnimatedNumber({ target, suffix = '' }: {
  target: number
  suffix?: string
}) {
  const ref = useRef(null)
  const isInView = useInView(ref, { once: true })
  const [count, setCount] = useState(0)

  useEffect(() => {
    if (!isInView) return

    const duration = 1500 // ms
    const steps = 60
    const increment = target / steps
    let current = 0

    const timer = setInterval(() => {
      current += increment
      if (current >= target) {
        setCount(target)
        clearInterval(timer)
      } else {
        setCount(Math.floor(current))
      }
    }, duration / steps)

    return () => clearInterval(timer)
  }, [isInView, target])

  return (
    <span ref={ref}>
      {count.toLocaleString()}{suffix}
    </span>
  )
}

Usage: <AnimatedNumber target={50000} suffix="+" /> renders as a counter that counts up from 0 to "50,000+" when the element scrolls into view. This is effective for stats like download counts, user numbers, and review counts.

How Do You Animate the Hero Section on Page Load?

The hero section should animate immediately on page load, not on scroll. Use a sequential animation with staggered delays:

'use client'
import { motion } from 'framer-motion'

export function Hero() {
  return (
    <section className="pt-24 pb-16 text-center">
      {/* Badge */}
      <motion.span
        initial={{ opacity: 0, y: 10 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.4, delay: 0 }}
        className="inline-block rounded-full border border-white/10
          bg-white/5 px-4 py-1.5 text-xs text-white/60"
      >
        New: Version 2.0
      </motion.span>

      {/* Headline */}
      <motion.h1
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5, delay: 0.1 }}
        className="mt-6 text-5xl font-bold tracking-tight"
      >
        Track Habits in 30 Seconds
      </motion.h1>

      {/* Subtitle */}
      <motion.p
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5, delay: 0.2 }}
        className="mt-4 text-lg text-white/60"
      >
        The simplest habit tracker for people who hate habit trackers.
      </motion.p>

      {/* CTA */}
      <motion.div
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 0.5, delay: 0.3 }}
        className="mt-8"
      >
        <a href="#" className="btn-primary px-8 py-3 rounded-xl">
          Download Free
        </a>
      </motion.div>
    </section>
  )
}

Each element has an increasing delay (0, 0.1, 0.2, 0.3 seconds), creating a cascading reveal from top to bottom. The total animation sequence takes about 0.8 seconds — fast enough to feel snappy, slow enough to be noticed.

How Do You Respect prefers-reduced-motion?

This is not optional. Users with motion sensitivity can enable "Reduce motion" in their operating system preferences. Your animations must respect this setting. Framer Motion has a built-in hook for this:

'use client'
import { useReducedMotion, motion } from 'framer-motion'

export function AnimatedCard({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      initial={shouldReduceMotion ? false : { opacity: 0, y: 20 }}
      whileInView={shouldReduceMotion ? {} : { opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.4 }}
    >
      {children}
    </motion.div>
  )
}

When shouldReduceMotion is true, the component renders immediately without animation. Setting initial to false tells Framer Motion to skip the initial state entirely and render the final state immediately.

A simpler global approach is to wrap your app in a MotionConfig provider:

import { MotionConfig } from 'framer-motion'

export default function Layout({ children }) {
  return (
    <MotionConfig reducedMotion="user">
      {children}
    </MotionConfig>
  )
}

The reducedMotion="user" setting automatically respects the operating system preference for all Framer Motion animations in your app. This is the recommended approach — set it once in your layout and every animation component automatically adapts.

What Performance Pitfalls Should You Avoid?

Animations can hurt performance if done carelessly. Watch out for:

  • Animating layout properties. Animating width, height, top, or left triggers browser layout recalculations (reflows) on every frame. Stick to opacity and transform properties (x, y, scale, rotate) which are GPU-accelerated.
  • Too many simultaneous animations. Animating 20+ elements at the same time can cause frame drops on mobile devices. Stagger animations and limit the number of active animations to 5-8 at any given moment.
  • Large image animations. Fading in a 2MB screenshot is expensive. Optimize images first (WebP, proper sizing), then animate.
  • Continuous animations. Spinning loaders, pulsing elements, or infinite loops consume CPU continuously, even when the user is not looking at them. Use sparingly and stop when out of viewport.

What Animation Patterns Does AppLander Include?

AppLander includes all of the animation patterns covered in this guide, pre-configured and optimized:

  • Hero section with staggered fade-up on page load
  • Feature grid with staggered scroll-reveal
  • Screenshot gallery with smooth carousel transitions
  • Social proof bar with animated counters
  • Hover effects on cards and buttons
  • Global prefers-reduced-motion support via MotionConfig
  • Performance-optimized with GPU-accelerated properties only

You do not need to implement any of these animations manually. They are part of the generated template, customizable through the config.ts file if you want to adjust timing, easing, or disable specific animations.

Ready to Animate Your App Landing Page?

Framer Motion makes it remarkably easy to add professional animations to a React-based landing page. The patterns in this guide — scroll reveals, staggered grids, hover effects, and carousels — cover everything an app landing page needs. Implement them thoughtfully, always respect reduced motion preferences, and keep performance in mind.

If you want these animations without the setup work, AppLander includes them out of the box. Generate your page, customize the content, and deploy — the animations are already there, tested, and optimized.

Share this article

Ready to ship your iOS app faster?

The Swift Kit gives you a production-ready SwiftUI codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Swift Kit