Skip to main content

Micro-Interactions

Micro-interactions are small, contained animations that respond to a single user action or system event. They are the difference between an interface that feels functional and one that feels polished. Every great app is full of them.

Anatomy of a Micro-Interaction

Every micro-interaction has four parts:

  1. Trigger — what starts it (user action or system event)
  2. Rules — what happens during the interaction
  3. Feedback — the visual or auditory response
  4. Loops & Modes — does it repeat or change over time?

A "like" button is a perfect example: the trigger is a tap, the rule changes the state from unliked to liked, the feedback is a heart animation, and the mode toggles between liked/unliked states.

Button Feedback

Buttons should feel physical. A button with no feedback leaves users wondering if their click registered:

/* Layered button feedback */
.button {
  position: relative;
  padding: 0.75rem 1.5rem;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 0.5rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, transform 0.1s ease;
  overflow: hidden;
}

.button:hover {
  background: #1d4ed8;
}

.button:active {
  transform: scale(0.97);
}

/* Ripple effect */
.button::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle, rgba(255,255,255,0.3) 10%, transparent 70%);
  opacity: 0;
  transform: scale(0);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.button:active::after {
  opacity: 1;
  transform: scale(2.5);
  transition: none;
}

For submit buttons that trigger async operations, show the loading state inline:

function SubmitButton({ isLoading, children }) {
  return (
    <motion.button
      disabled={isLoading}
      whileTap={isLoading ? {} : { scale: 0.97 }}
      className="button"
    >
      {isLoading ? (
        <motion.span
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          className="flex items-center gap-2"
        >
          <span className="spinner" /> Saving...
        </motion.span>
      ) : (
        children
      )}
    </motion.button>
  );
}

Toggle Switches

A toggle switch should feel satisfying to flip:

.toggle {
  width: 52px;
  height: 28px;
  border-radius: 14px;
  background: #d1d5db;
  padding: 2px;
  cursor: pointer;
  transition: background 0.2s ease;
}

.toggle.active {
  background: #2563eb;
}

.toggle-knob {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.toggle.active .toggle-knob {
  transform: translateX(24px);
}

The cubic-bezier(0.4, 0, 0.2, 1) curve gives the knob a snappy, confident movement that feels like a real switch.

Loading States

Loading indicators should set expectations without causing anxiety:

Skeleton Screens

Replace loading spinners with skeleton screens that show the shape of upcoming content:

.skeleton-text {
  height: 1rem;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
  border-radius: 0.25rem;
}

.skeleton-text:nth-child(2) {
  width: 80%;
}

.skeleton-text:nth-child(3) {
  width: 60%;
}

Progress Indicators

For operations with known progress, show a progress bar:

.progress-bar {
  height: 4px;
  background: #e5e7eb;
  border-radius: 2px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: #2563eb;
  border-radius: 2px;
  transition: width 0.3s ease;
}

Notification Animations

Notifications should enter distinctly, sit quietly, and leave gracefully:

function Toast({ message, type, onDismiss }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: -20, scale: 0.95 }}
      animate={{ opacity: 1, y: 0, scale: 1 }}
      exit={{ opacity: 0, y: -10, scale: 0.95 }}
      transition={{ duration: 0.2, ease: 'easeOut' }}
      className={`toast toast-${type}`}
    >
      <span>{message}</span>
      <button onClick={onDismiss} aria-label="Dismiss">
        &times;
      </button>
    </motion.div>
  );
}

Stack multiple toasts with staggered positioning:

function ToastContainer({ toasts }) {
  return (
    <div className="fixed top-4 right-4 flex flex-col gap-2 z-50">
      <AnimatePresence>
        {toasts.map((toast) => (
          <Toast key={toast.id} {...toast} />
        ))}
      </AnimatePresence>
    </div>
  );
}

Scroll-Triggered Animations

Elements that animate as they enter the viewport create a dynamic scrolling experience. Use the Intersection Observer API:

import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';

function ScrollReveal({ children }) {
  const ref = useRef(null);
  const isInView = useInView(ref, { once: true, margin: '-100px' });

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 30 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.5, ease: 'easeOut' }}
    >
      {children}
    </motion.div>
  );
}

The once: true option means the animation plays only the first time the element enters the viewport. The margin: '-100px' triggers the animation slightly before the element is fully visible, so it feels seamless.

Guidelines for scroll animations:

  • Animate once — repeating animations on every scroll direction is distracting
  • Keep them subtle — a 20-30px translateY and opacity change is enough
  • Stagger sibling elements — if multiple elements enter at once, stagger by 50-100ms
  • Skip them above the fold — content visible on page load should not animate in

Designing Micro-Interactions

Before implementing, ask these questions:

  1. Does this animation serve a purpose? If it does not provide feedback, orientation, or delight — cut it.
  2. Is it fast enough? Most micro-interactions should be 100-300ms. If users notice the animation, it might be too slow.
  3. Does it respect reduced motion? Every micro-interaction needs a reduced-motion fallback.
  4. Is it consistent? Similar interactions should have similar animations throughout your app.
  5. Does it scale? An animation that looks great with 3 items might be overwhelming with 30.

The best micro-interactions are invisible. Users do not notice them consciously — the interface just feels right.