Framer Motion animation patterns for MeritGrid's premium SaaS UI. Covers page transitions, micro-animations, modal animations, hover effects, and design token integration.
framer-motion ^12.29.2styles/tokens/"use client" components.Use motion.div with AnimatePresence for route transitions:
"use client";
import { motion, AnimatePresence } from "framer-motion";
export function PageWrapper({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.25, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
"use client";
import { motion, AnimatePresence } from "framer-motion";
const backdropVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
};
const modalVariants = {
hidden: { opacity: 0, scale: 0.96, y: 16 },
visible: { opacity: 1, scale: 1, y: 0 },
exit: { opacity: 0, scale: 0.96, y: 8 },
};
export function Modal({ isOpen, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="modal-backdrop" // z-index: 30
variants={backdropVariants}
initial="hidden"
animate="visible"
exit="hidden"
transition={{ duration: 0.2 }}
/>
<motion.div
className="modal-dialog" // z-index: 40
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}
Stagger children for list rendering:
"use client";
import { motion } from "framer-motion";
const containerVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.08 } },
};
const itemVariants = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease: "easeOut" } },
};
export function ScholarshipList({ items }: Props) {
return (
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{/* card content */}
</motion.li>
))}
</motion.ul>
);
}
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 400, damping: 20 }}
>
Apply Now
</motion.button>
"use client";
import { motion } from "framer-motion";
export function SkeletonCard() {
return (
<motion.div
className="skeleton-card"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
/>
);
}
Framer Motion's transform property creates new stacking contexts. Always pair with correct z-index tokens:
/* In CSS modules or global styles */
.modal-backdrop { z-index: var(--z-modal-backdrop); } /* 30 */
.modal-dialog { z-index: var(--z-modal); } /* 40 */
.toast { z-index: var(--z-toast); } /* 50 */
will-change: transform only when necessary (can cause memory issues).opacity and transform for animations (GPU-accelerated).width, height, or margin — use scaleX/scaleY instead.layoutId for shared element transitions (e.g., tab underline animations).Any component using Framer Motion MUST have "use client" at the top. Never use motion components in Server Components.