skip to note
back to writing

wfd / Oct 10, 2025 / 2 min

i deleted framer motion and nothing broke

Legacy This WFD was imported from old blog posts scattered across my files. It may still be relevant or it may not. Kept for the record.

My portfolio used to run on Framer Motion. Every hover effect, every entrance animation, every subtle transition—powered by a 50kb JavaScript library.

Then I noticed animations stuttering on my old MacBook Air. JavaScript animations run on the main thread, competing with rendering and user input. CSS transforms run on the GPU. Different thread, smoother animations.

So I deleted Framer Motion. The site got faster and nothing broke.

the conversion

25 components. Same pattern every time: replace Framer Motion components with HTML elements, swap animation props for CSS classes.

Hover effects:

// before
<motion.button
  whileHover={{ scale: 1.02 }}
  whileTap={{ scale: 0.98 }}
>
  click me
</motion.button>

// after
<button className="hover:scale-[1.02] active:scale-[0.98] transition-transform duration-200">
  click me
</button>

Entrance animations needed a hook:

export function useEntranceAnimation(delay = 0) {
  const [isVisible, setIsVisible] = useState(false)
  const ref = useRef(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          setTimeout(() => setIsVisible(true), delay)
          observer.unobserve(entries[0].target)
        }
      },
      { threshold: 0.1, rootMargin: '-40% 0px' }
    )

    if (ref.current) observer.observe(ref.current)
    return () => observer.disconnect()
  }, [delay])

  return {
    ref,
    className: isVisible
      ? 'opacity-100 translate-y-0'
      : 'opacity-0 translate-y-5'
  }
}

Intersection Observer detects viewport entry. CSS classes trigger transitions. GPU handles the actual animation.

the hard part

AnimatePresence was tricky. Framer Motion handles mount/unmount transitions automatically. I had to build a custom component that manages visibility state and applies CSS transitions during both enter and exit phases.

Worth it though. No more framework magic I couldn't debug.

the results

MetricBeforeAfter
Bundle size+50kb0
Animation threadMainGPU
Scroll stutteringSometimesNever
Hover jankOccasionalNone

First Contentful Paint improved. Time to Interactive improved. The site just feels snappier.

bonus: centralized timing

All animations now use CSS custom properties:

:root {
  --motion-base: 200ms;
  --motion-ease-ios: cubic-bezier(0.22, 1, 0.36, 1);
}

.animate {
  transition: transform var(--motion-base) var(--motion-ease-ios);
}

One variable change adjusts the feel of every animation on the site.

when framer motion makes sense

Complex gesture sequences. Physics-based springs. Coordinated multi-element animations. If you're building something like Stripe's homepage, use Framer Motion.

For a portfolio with hover effects and fade-ins? CSS is enough. The platform already has everything you need.

50kb sounds small. But it's 50kb to download, parse, and execute on every visit. Dependencies compound. Sometimes the best code is the code you delete.