Tooni Olaniyan

Software (Frontend) Engineer

Vibrant UI animation graphic
Dynamic UI elements in action

Motion UI & Micro-Interactions: Making Frontend Apps Pop

In 2025, users expect web apps to feel alive and intuitive. Motion UI and micro-interactions like a button bounce or a smooth fade-in are game-changers for engagement. Using tools like Framer Motion which I believe is now jsut "Motion", you can add flair to your web apps and websites without tanking performance. Here’s how I’ve used these techniques to level up UX, with some practical code to get you started.


What’s Motion UI and Why Care?

Motion UI is all about animations that make interfaces feel dynamic—think page transitions or hover effects. Micro-interactions are those tiny touches, like a button pulsing when clicked or a form glowing on focus. They guide users, confirm actions, and add personality.

Why bother? In e-commerce, a slick "Add to Cart" animation can boost conversions. In dashboards, animated charts make data feel less dry. But overdo it, and you risk sluggish performance. Let’s see how Framer Motion keeps things smooth.


Framer Motion: Your Animation BFF

Framer Motion is a React library that makes animations dead simple. It’s lightweight, supports gestures, and plays nice with Next.js. Here’s a quick example: a "magnetic text container".


import React, { useState, useEffect, useRef } from 'react'
import {
  motion,
  useMotionValue,
  useSpring,
  type SpringOptions,
} from 'motion/react'

const SPRING_CONFIG = { stiffness: 26.7, damping: 4.1, mass: 0.2 }

export type MagneticProps = {
  children: React.ReactNode
  intensity?: number
  range?: number
  actionArea?: 'self' | 'parent' | 'global'
  springOptions?: SpringOptions
}

export function Magnetic({
  children,
  intensity = 0.6,
  range = 100,
  actionArea = 'self',
  springOptions = SPRING_CONFIG,
}: MagneticProps) {
  const [isHovered, setIsHovered] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  const x = useMotionValue(0)
  const y = useMotionValue(0)

  const springX = useSpring(x, springOptions)
  const springY = useSpring(y, springOptions)

  useEffect(() => {
    const calculateDistance = (e: MouseEvent) => {
      if (ref.current) {
        const rect = ref.current.getBoundingClientRect()
        const centerX = rect.left + rect.width / 2
        const centerY = rect.top + rect.height / 2
        const distanceX = e.clientX - centerX
        const distanceY = e.clientY - centerY

        const absoluteDistance = Math.sqrt(distanceX ** 2 + distanceY ** 2)

        if (isHovered && absoluteDistance <= range) {
          const scale = 1 - absoluteDistance / range
          x.set(distanceX * intensity * scale)
          y.set(distanceY * intensity * scale)
        } else {
          x.set(0)
          y.set(0)
        }
      }
    }

    document.addEventListener('mousemove', calculateDistance)

    return () => {
      document.removeEventListener('mousemove', calculateDistance)
    }
  }, [ref, isHovered, intensity, range])

  useEffect(() => {
    if (actionArea === 'parent' && ref.current?.parentElement) {
      const parent = ref.current.parentElement

      const handleParentEnter = () => setIsHovered(true)
      const handleParentLeave = () => setIsHovered(false)

      parent.addEventListener('mouseenter', handleParentEnter)
      parent.addEventListener('mouseleave', handleParentLeave)

      return () => {
        parent.removeEventListener('mouseenter', handleParentEnter)
        parent.removeEventListener('mouseleave', handleParentLeave)
      }
    } else if (actionArea === 'global') {
      setIsHovered(true)
    }
  }, [actionArea])

  const handleMouseEnter = () => {
    if (actionArea === 'self') {
      setIsHovered(true)
    }
  }

  const handleMouseLeave = () => {
    if (actionArea === 'self') {
      setIsHovered(false)
      x.set(0)
      y.set(0)
    }
  }

  return (
    <motion.div
      ref={ref}
      onMouseEnter={actionArea === 'self' ? handleMouseEnter : undefined}
      onMouseLeave={actionArea === 'self' ? handleMouseLeave : undefined}
      style={{
        x: springX,
        y: springY,
      }}
    >
      {children}
    </motion.div>
  )
}

This component allows it children (text) feel magnetic when on hover. Perfect for a simple website button text.


Dashboards: Animating Data

For dashboards, animations can make data feel alive. Here’s a bar chart that slides in smoothly:

import { motion } from 'framer-motion'

const DataChart = () => {
  const chartVariants = {
    hidden: { opacity: 0, y: 20 },
    visible: {
      opacity: 1,
      y: 0,
      transition: { duration: 0.5, staggerChildren: 0.1 },
    },
  }

  const barVariants = {
    hidden: { height: 0 },
    visible: { height: '100%', transition: { duration: 0.4 } },
  }

  return (
    <motion.div
      className="flex gap-2"
      variants={chartVariants}
      initial="hidden"
      animate="visible"
    >
      {[50, 80, 30, 90, 60].map((height, index) => (
        <motion.div
          key={index}
          className="w-10 bg-blue-600"
          style={{ height: `${height}px` }}
          variants={barVariants}
        />
      ))}
    </motion.div>
  )
}

export default DataChart

The bars grow one by one, drawing attention to key metrics without overwhelming users.


Keeping Performance Snappy

Animations are cool, but they can slow things down if you’re not careful. Here’s how to keep it fast:

  • Stick to GPU-Friendly Stuff: Use transform (like scale or translate) and opacity for smooth animations.
  • Don’t Go Overboard: Limit animations to key elements like buttons or cards.
  • Lazy-Load Animations: Trigger them only when in view with Framer Motion’s useInView:
import { motion, useInView } from 'framer-motion'
import { useRef } from 'react'

const LazyCard = () => {
  const ref = useRef(null)
  const isInView = useInView(ref, { once: true })

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 30 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.4 }}
      className="rounded bg-gray-200 p-4"
    >
      Animated Card
    </motion.div>
  )
}
  • Respect Accessibility: Use prefers-reduced-motion to skip animations for sensitive users:
import { motion, useReducedMotion } from 'framer-motion'

const AccessibleButton = () => {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.button
      className="rounded bg-green-500 px-4 py-2 text-white"
      whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}
      whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}
    >
      Click Me
    </motion.button>
  )
}

Wrapping Up

Motion UI and micro-interactions make your app feel polished and user-friendly. With Framer Motion, you can add subtle animations to your apps that delight without dragging down performance. Start with a button or a chart, keep it snappy, and watch your UX shine.


Food for Thought

  • What’s a micro-interaction you love in an app you use?
  • How do you keep animations from feeling “too much”?

More to Explore