Motion

More and more websites and applications are incorporating motion into their designs and interactions. Motion can enhance the user experience by providing visual feedback, guiding attention, and making interactions feel more natural and engaging. However, I believe we should carefully consider when and how to use motion. The design principles I strive for prioritize simplicity, minimalism, and maintainability.

In this article, I will walk you through the core concepts behind building UI animations with Motion (previously known as Framer Motion), and show you four concrete examples I built to put those ideas into practice.

#Core Concepts

Before diving into the examples, it helps to understand the handful of primitives that Motion gives you. Once you internalize these, you will be able to compose almost any animation imaginable.

1. motion.* components

The foundation of every Motion animation is swapping a plain HTML element for its motion.* equivalent:

// Before
<div className="card" />

// After – now it can be animated
<motion.div className="card" />

motion.div, motion.span, motion.svg, and friends accept extra props (animate, initial, exit, transition, whileHover, whileTap, …) that describe how the element should move.

<div>

div

Snaps instantly

<motion.div>

motion

Smooth transition

motion.* components
Sidenote:
The plain div on the left snaps to its new style instantly — no interpolation happens. The motion.div on the right smoothly drives between states using the browser's animation engine. Hit Trigger to compare.

2. animate and initial

initial sets the element's starting state. animate describes the target state Motion will smoothly drive towards.

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
/>

When the component mounts, it fades in while sliding upward by 20 px. No manual useEffect or requestAnimationFrame required.

initial={{ opacity: 0, y: 32 }}
animate={{ opacity: 1, y: 0 }}
Hello, I animated in!
animate & initial
Sidenote:
initial is the off-screen starting pose. animate is where it lands. Hit Play to watch the card animate from { opacity: 0, y: 32 } to { opacity: 1, y: 0 }.

3. transition

transition controls how the animation plays — duration, easing curve, delay, and type. Motion supports three physics models:

TypeBehaviour
tweenClassic CSS-like easing (linear, easeIn, easeOut, easeInOut)
springPhysics-based spring with stiffness, damping, and mass
stiff springHigh stiffness, low damping — overshoots and bounces back
// Smooth CSS-style
transition={{ type: "tween", duration: 0.6, ease: "easeInOut" }}

// Natural spring
transition={{ type: "spring", stiffness: 300, damping: 20 }}

// Bouncy spring
transition={{ type: "spring", stiffness: 600, damping: 10 }}
tween
spring
stiff spring
transition types
Sidenote:
All three dots travel the same distance to the same destination — only the transition differs. Notice how the stiff spring overshoots and bounces back; that subtle overshoot is what makes spring animations feel alive.

4. layout prop

Adding layout to a motion.* element tells Motion to automatically animate whenever its size or position changes in the DOM. This is the secret ingredient behind smooth accordion expansions, list reordering, and collapsing cards — you simply change state and Motion handles the interpolation.

<motion.div layout animate={{ height: isOpen ? 96 : 48 }}>
  {isOpen && <ExpandedContent />}
</motion.div>

without layout

Card A
Card B
Card C

with layout

Card A
Card B
Card C

Click a card to expand it

layout prop
Sidenote:
Click a card on either column to expand it. Withoutlayout, siblings snap to their new position instantly. Withlayout, everything glides into place — no manual height calculation needed.

5. AnimatePresence and exit

React removes elements from the DOM instantly. AnimatePresence wraps a group of conditionally rendered elements and gives them a chance to play an exit animation before they are removed.

<AnimatePresence mode="wait">
  {isVisible && (
    <motion.div
      key="my-element"
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
    />
  )}
</AnimatePresence>

The mode="wait" option ensures the outgoing element finishes its exit animation before the incoming one starts — preventing two elements from overlapping.

mount & unmount with exit animation

I animate in and out

tab swap with mode="wait"

Home content
AnimatePresence & exit
Sidenote:
The top section shows a basic mount / unmount cycle with an exit scale animation. The bottom section uses mode="wait" for a directional tab swap — the outgoing tab slides out before the incoming one slides in.

6. Gesture props

Motion exposes ergonomic props for common user interactions:

<motion.button
  whileHover={{ scale: 1.08, y: -4 }}
  whileTap={{ scale: 0.92, y: 2 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>

These feel far more alive than a CSS transition: transform 0.2s because they respond in real time to the user's pointer speed.

hover me

whileHover

tap me

whileTap

both

both

whileHover & whileTap
Sidenote:
Hover and tap each card to feel how whileHover and whileTap compose together. The spring transition makes the scale feel responsive to the speed of your interaction, not just the state change.

The Mental Model

Think of a Motion animation as three questions:

  1. Where does it start?initial
  2. Where does it end up?animate
  3. How does it get there?transition

Everything else (exit, layout, whileHover, …) is just layering more answers on top of those three questions.

#Dynamic Island

Apple's Dynamic Island on iPhone 14 Pro is a brilliant example of purposeful motion — a hardware notch that transforms into context-aware UI. The demo below recreates that concept: a pill-shaped container that expands to show a silent / ring notification, then collapses back.

Sidenote:
The pill morphs its width and height using animate, while AnimatePresence mode="wait" crossfades between three content states. The looping bell shake is driven by a repeat: Infinity rotation sequence.

How it works

The key technique here is animating width and height directly on a motion.div combined with layout:

<motion.div
  layout
  animate={{
    width: expansionState === "collapsed" ? 120 : 200,
    height: expansionState === "collapsed" ? 36 : 50,
  }}
  transition={{ duration: 0.4, ease: "easeInOut" }}
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.98 }}
>

Inside the pill, AnimatePresence mode="wait" swaps between three content views (collapsed, expanded, controls) with crossfade transitions. Each view is keyed so React and Motion can track which element is entering and which is leaving.

The bell icon in the ring state uses a looping rotate animation to simulate ringing:

animate={{
  rotate: [-5, 5, -5, 5, 0],
}}
transition={{
  duration: 0.4,
  repeat: Infinity,
  repeatDelay: 0.3,
}}

Concept to remember: Combine layout + animate size changes + AnimatePresence for any component that morphs its shape in response to state.


#Notifications

The notification stack collapses multiple cards into a "fanned" pile when hidden, then fans out into a readable list when expanded — a pattern used by iOS, macOS, and many notification centre UIs.

Notifications

✌️

MyUSC1 ·

just now

USC
✌️

MyUSC2 ·

4m ago

USC
✌️

MyUSC3 ·

6m ago

USC
Sidenote:
Each card gets a progressively larger y offset, smaller scaleX, and lower opacity when collapsed — creating a stacked depth illusion with a single boolean. Tap any card or the Collapse button to toggle.

How it works

Each notification card is a motion.div with an independent y (vertical offset) and optional scaleX / opacity target driven by the show boolean:

// Second card — partially visible behind the first
<motion.div
  animate={{
    y: show ? 0 : -100,
    scaleX: show ? 1 : 0.95,
    opacity: show ? 1 : 0.75,
  }}
  transition={{ type: "tween", duration: 0.4 }}
/>

// Third card — deeper in the stack
<motion.div
  animate={{
    y: show ? 0 : -200,
    scaleX: show ? 1 : 0.9,
    opacity: show ? 1 : 0.5,
  }}
  transition={{ type: "tween", duration: 0.6 }}
/>

By giving each card a progressively larger y offset and a slightly longer duration, the cards collapse into a satisfying stacked silhouette at different speeds — the bottom card "catches up" last, which feels natural.

Concept to remember: Stagger depth effects by varying y, scaleX, opacity, and duration across sibling elements driven by the same boolean. No staggerChildren needed for simple cases.


#Paginations

A custom animated pagination indicator that tracks which carousel slide is active. The active dot expands into a label pill while the inactive dots shrink — a pattern popularised by the Apple AirPods Pro feature carousel.

Girl with a Pearl Earring

Girl with a Pearl Earring

Johannes Vermeer, 1665

The Starry Night

The Starry Night

Vincent van Gogh, 1889

The Great Wave

The Great Wave

Katsushika Hokusai, 1831

The Persistence of Memory

The Persistence of Memory

Salvador Dalí, 1931

  • Girl with a Pearl Earring
Sidenote:
The active indicator grows via a type: "spring" scale and reveals its text label. All inactive dots shrink and lose their text. Dragging the carousel or clicking a dot both update the same current state.

How it works

The indicator is a list of motion.li elements. Each item's scale is driven by whether its index matches current:

<motion.li
  animate={{ scale: current === index ? 1 : 0.8 }}
  transition={{
    type: "spring",
    stiffness: 300,
    damping: 20,
  }}
  className={current === index
    ? "min-w-fit px-3 py-2 bg-white rounded-full text-xs shadow-md"
    : "w-3 h-3 bg-gray-300 rounded-full"}
  onClick={() => api?.scrollTo(index)}
>
  {current === index ? text : ""}
</motion.li>

The carousel itself uses shadcn/ui's Carousel under the hood, and the CarouselApi event "select" keeps current in sync:

api.on("select", () => {
  setCurrent(api.selectedScrollSnap())
})

Concept to remember: Spring transitions on scale are perfect for indicator dots — they feel bouncy and alive without being distracting. Always use a type: "spring" transition for indicators and icon state changes.


#Payment

A multi-step payment flow where the form card pops in from nothing, the button label morphs through states ("Make Payment" → "Send" → "Sent"), and a checkmark SVG path draws itself on completion.

Sidenote:
The modal scales in from scale: 0 via AnimatePresence. On confirmation, pathLength animates from 01 on the SVG checkmark path, producing a "draw-on" effect without any canvas or CSS tricks.

How it works

Step 1 — Modal entrance: AnimatePresence wraps the form, which scales in from scale: 0 to scale: 1:

<AnimatePresence initial={true}>
  {showModal && (
    <motion.div
      key="box"
      initial={{ opacity: 0, scale: 0 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0 }}
      transition={{ duration: 0.3, type: "tween" }}
    />
  )}
</AnimatePresence>

Step 2 — SVG path drawing: This is the most visually striking trick. Motion can animate pathLength on an SVG <path>, which makes it look like the path is being drawn in real time:

<motion.svg
  initial={{ pathLength: 0 }}
  animate={{ pathLength: 1 }}
  exit={{ pathLength: 0 }}
  transition={{ duration: 0.5 }}
>
  <motion.path d="M20 6L9 17l-5-5" />
</motion.svg>

pathLength goes from 0 (invisible) to 1 (fully drawn). Combined with AnimatePresence, the checkmark draws itself when the payment is confirmed and undraws itself when reset.

Concept to remember: pathLength animation on motion.path is one of Motion's hidden gems. It works on any SVG path and creates a "draw-on" effect that feels premium without any canvas or CSS tricks.

#Summary

Here is a quick reference of the Motion concepts used across all examples:

ConceptUsed in
animate + layout (size morphing)Dynamic Island
AnimatePresence mode="wait" (content swap)Dynamic Island, Payment
whileHover / whileTap (gesture feedback)Dynamic Island, Gesture demo
Staggered y + opacity offsetsNotifications
type: "spring" on scalePaginations, Transition demo
pathLength SVG drawingPayment
initial + animate (entrance)Animate demo, all components

The biggest shift when adopting Motion is moving from CSS-driven to state-driven animations. Instead of writing keyframes or managing class toggles, you describe the desired end state in animate, and Motion figures out how to get there. That mental model — what rather than how — is what makes Motion so ergonomic for building production UI.

Inspired by ui.lndev.me and the Motion documentation.