ays import from the motion/react subset. This enables aggressive tree-shaking, reducing the bundle by ~60% compared to the full library.
- Adopt
.lottie Format: Migrate all .json assets to .lottie. This binary format reduces file sizes by approximately 75% and offers faster parsing.
- Isolate SSR Risks: Lottie relies on canvas/SVG manipulation that breaks server-side rendering. Create a wrapper component that defers rendering until the client is hydrated.
- Compose, Don't Replace: Use Framer Motion to animate the container of a Lottie animation. This allows the illustration to participate in layout animations and entrance/exit sequences.
2. Implementation: Hybrid Notification System
This example demonstrates a notification toast where Framer Motion handles the slide-in/slide-out transition and layout management, while Lottie renders the complex status icon.
import { motion, AnimatePresence } from 'motion/react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
import { useState } from 'react';
interface ToastProps {
id: string;
message: string;
type: 'success' | 'error';
onDismiss: (id: string) => void;
}
export function NotificationToast({ id, message, type, onDismiss }: ToastProps) {
const iconSource = type === 'success'
? '/assets/confirm-check.lottie'
: '/assets/alert-triangle.lottie';
return (
<motion.div
layout
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.9 }}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
className="toast-container"
role="alert"
>
<div className="toast-icon">
<DotLottieReact
src={iconSource}
loop={false}
autoplay
style={{ width: 24, height: 24 }}
aria-hidden="true"
/>
</div>
<span className="toast-message">{message}</span>
<motion.button
whileTap={{ scale: 0.9 }}
onClick={() => onDismiss(id)}
className="toast-close"
aria-label="Dismiss notification"
>
β
</motion.button>
</motion.div>
);
}
export function ToastStack({ toasts }: { toasts: ToastProps[] }) {
return (
<div className="toast-stack">
<AnimatePresence>
{toasts.map((toast) => (
<NotificationToast key={toast.id} {...toast} />
))}
</AnimatePresence>
</div>
);
}
Rationale:
layout prop on the container ensures that when a toast is dismissed, remaining toasts smoothly reflow without manual calculation.
DotLottieReact is used instead of legacy wrappers for better performance and smaller bundle size.
aria-hidden="true" is applied to the Lottie component to prevent screen readers from announcing binary animation data, while the container maintains the role="alert".
3. Implementation: SSR-Safe Lottie Wrapper
To prevent hydration mismatches, encapsulate Lottie loading in a client-only wrapper.
import { useEffect, useState, type ComponentType } from 'react';
type DotLottieComponent = ComponentType<{
src: string;
loop?: boolean;
autoplay?: boolean;
style?: React.CSSProperties;
[key: string]: any;
}>;
export function useClientLottie() {
const [LottiePlayer, setLottiePlayer] = useState<DotLottieComponent | null>(null);
useEffect(() => {
import('@lottiefiles/dotlottie-react').then((mod) => {
setLottiePlayer(() => mod.DotLottieReact);
});
}, []);
return LottiePlayer;
}
export function SafeLottiePlayer(props: React.ComponentProps<DotLottieComponent>) {
const Player = useClientLottie();
if (!Player) {
return <div style={props.style} aria-hidden="true" />;
}
return <Player {...props} />;
}
Rationale:
- Dynamic import ensures the Lottie library is only loaded on the client.
- The placeholder
div maintains layout stability during SSR, preventing content shift when the client hydrates.
- This pattern allows Lottie components to be used safely in Next.js or Remix applications without
suppressHydrationWarning hacks.
Pitfall Guide
-
The "Spinner Trap"
- Explanation: Using Lottie for simple loading spinners or progress indicators. This introduces a heavy library dependency and network request for a motion that can be achieved with CSS or a few lines of Framer Motion.
- Fix: Use CSS
@keyframes or motion.div with animate={{ rotate: 360 }} for simple rotations. Reserve Lottie for complex, multi-element illustrations.
-
Hydration Mismatch Errors
- Explanation: Rendering Lottie components directly in server components causes hydration failures because the canvas/SVG output differs between server and client.
- Fix: Always use dynamic imports or the
useClientLottie pattern shown above. Ensure the component only renders after the client is ready.
-
Legacy Format Bloat
- Explanation: Continuing to use
.json Lottie files. These are text-based and significantly larger than the binary .lottie format.
- Fix: Convert all assets to
.lottie using the LottieFiles converter. Update imports to use @lottiefiles/dotlottie-react which supports the optimized format.
-
Importing Full Framer Motion
- Explanation: Importing from
framer-motion pulls in the entire library, including legacy APIs and unused features, increasing bundle size.
- Fix: Always import from
motion/react. This subset is optimized for React and tree-shakes unused code, reducing the footprint to ~17KB gzipped.
-
Layout Thrashing with Framer
- Explanation: Animating
width or height properties directly causes layout thrashing and poor performance.
- Fix: Use the
layout prop for automatic layout animations. For size changes, prefer scale transforms or animate width/height only when necessary and combined with will-change.
-
Accessibility Neglect
- Explanation: Lottie animations are often ignored by assistive technologies, or screen readers announce raw animation data.
- Fix: Wrap Lottie components in a container with appropriate ARIA roles. Set
aria-hidden="true" on the Lottie element itself. Ensure motion can be paused via prefers-reduced-motion media queries.
-
Over-Orchestrating Lottie Playheads
- Explanation: Attempting to manually sync Lottie playheads to scroll position or drag gestures using
requestAnimationFrame and seek. This is error-prone and performance-heavy.
- Fix: If the animation needs to be driven by user interaction or scroll, use Framer Motion. Lottie is best for pre-defined sequences. Use Framer's
useScroll or useDrag for interactive motion.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Hero Section Illustration | Lottie | Requires complex vector art and brand fidelity. | Asset size only; no JS overhead if lazy loaded. |
| Modal/Drawer Transition | Framer Motion | Needs layout animation and state synchronization. | Fixed bundle cost (~17KB). |
| Drag-and-Drop List | Framer Motion | Requires gesture handling and spring physics. | Fixed bundle cost. |
| Animated Icon (Simple) | Framer Motion / CSS | Low complexity; code-based is lighter. | Zero additional cost. |
| Animated Icon (Complex) | Lottie | Designer-created path morphing or particles. | Asset size; use .lottie format. |
| Scroll-Driven Progress | Framer Motion | Native useScroll integration. | Fixed bundle cost. |
| Form Submission Feedback | Hybrid | Framer for container, Lottie for success/error icon. | Asset + Fixed bundle. |
Configuration Template
Use this TypeScript configuration to standardize motion settings and ensure consistent behavior across the application.
// motion.config.ts
import { MotionConfig as BaseMotionConfig } from 'motion/react';
import { useEffect, useState } from 'react';
export function MotionProvider({ children }: { children: React.ReactNode }) {
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduceMotion(mediaQuery.matches);
const handler = (e: MediaQueryListEvent) => setReduceMotion(e.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);
return (
<BaseMotionConfig reducedMotion={reduceMotion ? 'user' : 'never'}>
{children}
</BaseMotionConfig>
);
}
Usage: Wrap your application root with <MotionProvider> to globally enforce reduced motion preferences. This ensures compliance with accessibility standards without manual checks in every component.
Quick Start Guide
- Install Dependencies:
npm install motion @lottiefiles/dotlottie-react
- Convert Assets: Export all After Effects animations to
.lottie format using the LottieFiles converter. Place them in your public assets directory.
- Create Wrapper: Implement the
useClientLottie hook and SafeLottiePlayer component as shown in the Core Solution.
- Build Hybrid Component: Create a component using
motion/react for structural animations and SafeLottiePlayer for illustrative content.
- Verify Performance: Run a Lighthouse audit and bundle analysis to confirm no hydration errors and optimal asset sizes.