Provides Framer Motion animation patterns and best practices specific to the Bellog blog project. Triggers when implementing animated components or interactions.
This skill provides the animation patterns and best practices used throughout the Bellog blog project.
// Interaction timings
const INTERACTION_FAST = 0.2; // Button press, hover start
const INTERACTION_NORMAL = 0.3; // Standard hover effects
const INTERACTION_SLOW = 0.5; // Modal open, drawer slide
// Transition timings
const TRANSITION_FAST = 0.3; // Quick state changes
const TRANSITION_NORMAL = 0.4; // Page transitions (standard)
const TRANSITION_SLOW = 0.6; // Heavy content transitions
// Ambient timings
const AMBIENT_SLOW = 3; // Slow blob movement
const AMBIENT_NORMAL = 4; // Standard blob rhythm
const AMBIENT_FAST = 5; // Faster ambient motion
Use for lists, grids, and groups of elements.
When to use: Animating multiple items that should appear in sequence
Example from Intro.tsx:
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1, // 100ms delay between each child
delayChildren: 0.2 // Start after 200ms
}
}
};
const item = {
hidden: { y: 20, opacity: 0 },
show: {
y: 0,
opacity: 1,
transition: {
duration: 0.5,
ease: "easeInOut"
}
}
};
// Usage
<motion.div
variants={container}
initial="hidden"
animate="show"
>
{items.map(item => (
<motion.div key={item.id} variants={item}>
{item.content}
</motion.div>
))}
</motion.div>
Use for decorative elements, background shapes, ambient animations.
When to use: Creating organic, perpetual movement
Example from Intro.tsx:
const blobVariants = {
initial: {
scale: 1,
x: 0,
y: 0
},
hover: {
scale: [1, 1.2, 0.9, 1.1, 1], // Keyframes
x: [0, 20, -10, 5, 0],
y: [0, -15, 10, -5, 0],
transition: {
duration: 4,
repeat: Infinity,
repeatType: "mirror",
ease: "easeInOut"
}
}
};
// Multiple blobs with different rhythms
const blob1 = { duration: 3, ... };
const blob2 = { duration: 4.5, ... };
const blob3 = { duration: 5.2, ... };
Best practices:
repeatType: "mirror" for smooth loopsUse for route changes, content swapping.
When to use: Navigating between pages or major content changes
Example from template.tsx:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{
ease: "easeInOut",
duration: 0.4
}}
>
{children}
</motion.div>
Variations:
// Fade only
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
// Slide from right
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
// Scale + fade
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
Use for buttons, cards, clickable elements.
When to use: Adding interactivity to user-actionable elements
const cardVariants = {
initial: { scale: 1 },
hover: {
scale: 1.02,
transition: {
duration: 0.3,
ease: "easeInOut"
}
},
tap: {
scale: 0.98
}
};
<motion.div
variants={cardVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
>
Common hover patterns:
Use for parallax, fade-ins, progress indicators.
When to use: Animations triggered by scroll position
import { useScroll, useTransform } from "framer-motion";
const { scrollYProgress } = useScroll();
const opacity = useTransform(scrollYProgress, [0, 0.5], [0, 1]);
const y = useTransform(scrollYProgress, [0, 0.5], [50, 0]);
<motion.div style={{ opacity, y }}>
{/* Content */}
</motion.div>
Example: Progress bar
const { scrollYProgress } = useScroll();
<motion.div
style={{ scaleX: scrollYProgress }}
className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left"
/>
Use with AnimatePresence for conditional rendering.
When to use: Elements that appear and disappear
import { AnimatePresence } from "framer-motion";
const variants = {
hidden: { opacity: 0, y: -10 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -10 }
};
<AnimatePresence>
{isVisible && (
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
transition={{ duration: 0.3 }}
>
{content}
</motion.div>
)}
</AnimatePresence>
Use these, not "linear":
// Bellog standard
ease: "easeInOut" // Default for most animations
// Other options
ease: "easeOut" // For entrances
ease: "easeIn" // For exits
ease: [0.43, 0.13, 0.23, 0.96] // Custom cubic-bezier
Before finalizing animations:
variants pattern (not inline animation props)easeInOut or appropriate alternativeprefers-reduced-motion if criticalTransform over top/left:
// ā
Good (GPU accelerated)
{ x: 100 }
{ translateX: "100px" }
{ scale: 1.1 }
// ā Bad (layout recalc)
{ left: 100 }
{ width: "100%" }
Will-change hint:
style={{ willChange: "transform" }}
Layout animations (use sparingly):
<motion.div layout>
Respect user preferences:
import { useReducedMotion } from "framer-motion";
const shouldReduceMotion = useReducedMotion();
const variants = {
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 20 },
visible: { opacity: 1, y: 0 }
};
ā Don't: Inline animation props
<motion.div animate={{ opacity: 1 }} initial={{ opacity: 0 }}>
ā Do: Use variants
const variants = { hidden: { opacity: 0 }, visible: { opacity: 1 } };
<motion.div variants={variants} initial="hidden" animate="visible">
ā Don't: Linear easing
transition={{ duration: 0.3, ease: "linear" }}
ā Do: Use curves
transition={{ duration: 0.3, ease: "easeInOut" }}
ā Don't: Animate width/height directly
animate={{ width: "100%" }}
ā Do: Use scale or layout
animate={{ scaleX: 1 }}
// or
<motion.div layout>
// Standard fade + slide in
{ initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 } }
// Standard hover
{ whileHover: { scale: 1.02 }, transition: { duration: 0.3 } }
// Standard stagger
{
variants: {
container: { transition: { staggerChildren: 0.1 } },
item: { hidden: { y: 20, opacity: 0 }, show: { y: 0, opacity: 1 } }
}
}
// Standard exit
{ exit: { opacity: 0, y: -10 }, transition: { duration: 0.2 } }
Remember: Animations should enhance the experience, not distract from it. When in doubt, keep it subtle and fast.