מדריך ליצירת אנימציות חלקות עם Framer Motion
למדו איך ליצור אנימציות יפות וביצועיות ב-React עם Framer Motion - מרכיבי motion בסיסיים ו-variants ועד אנימציות מופעלות גלילה ותזמור מורכב.

# A Guide to Smooth Animations with Framer Motion
Animations are the difference between a website that works and a website that feels alive. They guide attention, communicate state changes, and create a sense of polish that users notice, even if they cannot articulate why. But animations done poorly -- janky, slow, or distracting -- are worse than no animations at all.
Framer Motion is a production-ready animation library for React that makes it remarkably easy to create smooth, performant animations. After using it extensively in my portfolio and client projects, I have developed a set of patterns and principles that I want to share.
## The Basics: Motion Components
Framer Motion works by wrapping standard HTML and SVG elements with motion components. A motion.div is a regular div with animation superpowers. The core API revolves around three props: initial, animate, and transition.
```tsx
"use client";
import { motion } from "framer-motion";
export function HeroSection() {
return (
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="mb-4 text-5xl font-bold tracking-tight md:text-7xl"
>
Full-Stack Developer
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
className="max-w-xl text-center text-lg text-neutral-600 dark:text-neutral-400"
>
Building modern web applications with React, TypeScript, and a passion
for clean, performant code.
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: "easeOut", delay: 0.4 }}
className="mt-8 flex gap-4"
>
);
}
```
The initial state is where the element starts, animate is where it ends, and transition controls how it gets there. In this example, the heading fades in and slides up first, followed by the paragraph 200ms later, and the buttons 400ms after that. The staggered timing creates a natural, cascading reveal.
A critical performance note: I only animate opacity and transform properties (which y, x, scale, and rotate map to). These properties are composited by the GPU and do not trigger layout recalculations. Never animate width, height, top, or left directly.
## Variants: Orchestrated Animations
When you have multiple elements that need coordinated animations, manually managing delays becomes tedious. Variants solve this elegantly. You define named animation states, and Framer Motion automatically propagates them through the component tree with configurable staggering.
```tsx
"use client";
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" },
},
};
interface Skill {
name: string;
level: number;
icon: string;
}
const skills: Skill[] = [
{ name: "React", level: 95, icon: "react" },
{ name: "TypeScript", level: 90, icon: "typescript" },
{ name: "Next.js", level: 92, icon: "nextjs" },
{ name: "Tailwind CSS", level: 88, icon: "tailwind" },
{ name: "Node.js", level: 85, icon: "nodejs" },
{ name: "Supabase", level: 82, icon: "supabase" },
];
export function SkillsGrid() {
return (
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
>
{skills.map((skill) => (
key={skill.name}
variants={itemVariants}
className="rounded-xl border border-neutral-200 p-6 dark:border-neutral-800"
>
initial={{ width: 0 }}
animate={{ width: transition={{ duration: 1, ease: "easeOut", delay: 0.5 }}
className="h-full rounded-full bg-blue-600"
/>
{skill.name}
${skill.level}% }}
{skill.level}%
))}
);
}
```
The containerVariants define staggerChildren: 0.1, which means each child's animation starts 100ms after the previous one. The children do not need to know about the stagger -- they just define their own hidden and visible states, and the parent orchestrates the timing. This creates a satisfying wave effect as the skill cards appear one by one.
## Scroll-Triggered Animations
Not all animations should fire on page load. Many elements are below the fold and should animate when they scroll into view. Framer Motion's whileInView prop handles this with built-in intersection observer integration.
```tsx
"use client";
import { motion } from "framer-motion";
interface Project {
id: string;
title: string;
description: string;
imageUrl: string;
tags: string[];
url: string;
}
export function ProjectCard({ project }: { project: Project }) {
return (
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6, ease: "easeOut" }}
whileHover={{ y: -4 }}
className="group overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-sm transition-shadow hover:shadow-lg dark:border-neutral-800 dark:bg-neutral-900"
>
src={project.imageUrl}
alt={project.title}
className="h-full w-full object-cover"
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.3 }}
/>
{project.title}
{project.description}
{project.tags.map((tag) => (
key={tag}
className="rounded-full bg-neutral-100 px-3 py-1 text-sm dark:bg-neutral-800"
>
{tag}
))}
);
}
export function ProjectsSection({ projects }: { projects: Project[] }) {
return (
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="mb-12 text-center text-3xl font-bold md:text-4xl"
>
Featured Projects
{projects.map((project) => (
))}
);
}
```
The viewport prop with once: true means the animation only plays the first time the element enters the viewport. The margin: "-100px" triggers the animation slightly before the element is fully visible, which feels more responsive. The whileHover prop adds a subtle lift effect on mouse hover, giving immediate tactile feedback.
## Page Transitions with AnimatePresence
Smooth page transitions make a single-page application feel cohesive rather than disjointed. AnimatePresence detects when components are removed from the React tree and lets them animate out before they disappear.
```tsx
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
const pageTransition = {
initial: { opacity: 0, y: 8 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.3, ease: "easeOut" },
},
exit: {
opacity: 0,
y: -8,
transition: { duration: 0.2, ease: "easeIn" },
},
};
export function PageTransitionWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
key={pathname}
variants={pageTransition}
initial="initial"
animate="animate"
exit="exit"
className="min-h-dvh"
>
{children}
);
}
```
The mode="wait" prop ensures the exiting page finishes its animation before the entering page begins. The key={pathname} tells React and Framer Motion that this is a new component instance when the route changes, triggering the exit and enter animations. I keep page transitions fast (under 300ms total) and subtle -- users should feel the flow, not watch an animation.
## Gesture Animations
Framer Motion excels at gesture-driven animations. The whileHover, whileTap, and drag props create interactive elements that respond to user input with zero boilerplate.
```tsx
"use client";
import { motion } from "framer-motion";
export function InteractiveButton({
children,
onClick,
}: {
children: React.ReactNode;
onClick?: () => void;
}) {
return (
onClick={onClick}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="rounded-full bg-blue-600 px-8 py-3 font-medium text-white"
>
{children}
);
}
export function DraggableCard() {
return (
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.1}
whileDrag={{ scale: 1.05, boxShadow: "0 20px 40px rgba(0,0,0,0.15)" }}
className="w-64 cursor-grab rounded-xl bg-white p-6 shadow-md active:cursor-grabbing dark:bg-neutral-800"
>
Drag me around
This card can be dragged within its constraints.
);
}
```
The spring physics on the button create a natural, bouncy feel that is far more satisfying than a linear transition. The drag constraints keep the card within bounds, and dragElastic controls how far it can be pulled past those bounds before snapping back.
## Animation Principles I Follow
After building dozens of animated interfaces, these are the rules I follow without exception:
1. Only animate transform and opacity - These are the only properties that do not trigger layout recalculation. This means x, y, scale, rotate, and opacity are your tools. Never animate width, height, top, left, or margin.
2. Keep durations under 200ms for feedback - Button presses, hover states, and toggles should feel instant. Anything over 200ms starts to feel sluggish for direct interaction feedback.
3. Use 300-600ms for reveals and transitions - Page transitions, scroll animations, and entrance effects can be slower because they are not responding to a direct user action.
4. Respect reduced motion preferences - Some users have vestibular disorders or simply prefer less motion. Always provide a reduced motion alternative.
5. Animate with purpose - Every animation should communicate something: a state change, a spatial relationship, or a hint about interactivity. If an animation does not serve a purpose, remove it.
6. Stagger, do not synchronize - When multiple elements animate in, stagger their timing. Simultaneous animations feel mechanical; staggered ones feel organic.
## Performance Considerations
Framer Motion is highly optimized, but you can still create performance problems if you are not careful. Here are the key considerations:
- Avoid animating during scroll - Use
whileInViewwithonce: trueinstead of continuous scroll-linked animations for most cases - Use
layoutanimations sparingly - Thelayoutprop triggers layout measurements on every frame; only use it when you genuinely need elements to animate between layout positions - Unmount animated components - Use
AnimatePresenceto properly clean up components rather than hiding them with opacity - Batch related animations - Use variants with
staggerChildreninstead of individualdelayprops, which gives Framer Motion more optimization opportunities
## Wrapping Up
Framer Motion transforms animation in React from a painful, imperative process into a declarative, composable one. The motion component API feels natural to React developers, variants handle complex orchestration elegantly, and the performance defaults are sensible.
The key is restraint. The best animations are the ones users feel but do not consciously notice. They make your application feel responsive, polished, and alive without drawing attention away from the content. Start with subtle opacity and transform animations, and only add complexity when it genuinely improves the user experience.
Want to see these animations in action? Browse this site -- every transition and hover effect uses the patterns described above. Have questions? Get in touch.
## מדריך ליצירת אנימציות חלקות עם Framer Motion
אנימציות הן ההבדל בין אתר שעובד לאתר שמרגיש חי. הן מנחות תשומת לב, מתקשרות שינויי מצב, ויוצרות תחושת ליטוש שמשתמשים מבחינים בה, גם אם לא יכולים להסביר למה. אבל אנימציות שנעשות בצורה גרועה - רוטטות, איטיות, או מסיחות - גרועות יותר מאשר אין אנימציות בכלל.
Framer Motion היא ספריית אנימציה מוכנה לפרודקשן עבור React שמקלה בצורה ניכרת על יצירת אנימציות חלקות וביצועיות. לאחר שימוש נרחב בה בתיק העבודות ובפרויקטים ללקוחות, פיתחתי סט דפוסים ועקרונות שאני רוצה לשתף.
### הבסיס: רכיבי Motion
Framer Motion עובד על ידי עטיפת אלמנטי HTML ו-SVG סטנדרטיים ברכיבי motion. motion.div הוא div רגיל עם כוחות-על של אנימציה. ממשק ה-API המרכזי מסתובב סביב שלושה props: initial, animate ו-transition.
```tsx
"use client";
import { motion } from "framer-motion";
export function HeroSection() {
return (
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="mb-4 text-5xl font-bold"
>
מפתח Full-Stack
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
className="max-w-xl text-center text-lg"
>
בניית יישומי ווב מודרניים עם React, TypeScript ותשוקה לקוד נקי וביצועי.
);
}
```
מצב initial הוא מקום התחלת האלמנט, animate הוא מקום הסיום, ו-transition שולט כיצד הוא מגיע לשם. בדוגמה זו, הכותרת מתעממת ומחליקה מעלה קודם, ואחריה הפסקה 200ms מאוחר יותר. התזמון המדורג יוצר חשיפה טבעית ומדורגת.
הערה ביצועית קריטית: אני מנמטת רק מאפייני opacity ו-transform (שאליהם ממפים y, x, scale ו-rotate). מאפיינים אלה מורכבים על ידי ה-GPU ואינם מפעילים חישובי פריסה מחדש. לעולם אל תנמטו width, height, top או left ישירות.
### Variants: אנימציות מתואמות
כשיש מספר אלמנטים שצריכים אנימציות מתואמות, ניהול ידני של עיכובים הופך מייגע. Variants פותרים זאת בצורה אלגנטית. מגדירים מצבי אנימציה בשם, ו-Framer Motion מפיץ אותם אוטומטית דרך עץ הרכיבים עם סטאגרינג הניתן להגדרה.
```tsx
"use client";
import { motion } from "framer-motion";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" },
},
};
export function SkillsGrid() {
return (
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
>
{skills.map((skill) => (
{skill.name}
initial={{ width: 0 }}
animate={{ width: transition={{ duration: 1, ease: "easeOut", delay: 0.5 }}
className="h-full rounded-full bg-blue-600"
/>
${skill.level}% }}
))}
);
}
```
ה-containerVariants מגדירים staggerChildren: 0.1, מה שאומר שהאנימציה של כל ילד מתחילה 100ms לאחר הקודם. הילדים לא צריכים לדעת על הסטאגרינג - הם פשוט מגדירים את מצבי hidden ו-visible שלהם, וההורה מתזמן. זה יוצר אפקט גל מספק כשכרטיסי המיומנויות מופיעים אחד אחד.
### אנימציות מופעלות גלילה
לא כל האנימציות צריכות לפעול בטעינת הדף. אלמנטים רבים נמצאים מתחת לקפל ועל ידי אנימציה עם גלילה לתצוגה. ה-prop whileInView של Framer Motion מטפל בזה עם אינטגרציה מובנית של intersection observer.
```tsx
"use client";
import { motion } from "framer-motion";
export function ProjectCard({ project }) {
return (
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6, ease: "easeOut" }}
whileHover={{ y: -4 }}
className="group overflow-hidden rounded-2xl border bg-white shadow-sm hover:shadow-lg"
>
{/ תוכן הכרטיס /}
);
}
```
ה-prop viewport עם once: true אומר שהאנימציה משוחקת רק בפעם הראשונה שהאלמנט נכנס לאזור הגלילה. ה-margin: "-100px" מפעיל את האנימציה מעט לפני שהאלמנט גלוי לחלוטין, מה שמרגיש יותר רספונסיבי.
### מעברי עמודים עם AnimatePresence
מעברי עמודים חלקים גורמים ליישום single-page להרגיש קוהרנטי. AnimatePresence מזהה מתי רכיבים מוסרים מעץ React ומאפשר להם לנמטת החוצה לפני שהם נעלמים.
```tsx
"use client";
import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation";
const pageTransition = {
initial: { opacity: 0, y: 8 },
animate: { opacity: 1, y: 0, transition: { duration: 0.3, ease: "easeOut" } },
exit: { opacity: 0, y: -8, transition: { duration: 0.2, ease: "easeIn" } },
};
export function PageTransitionWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
key={pathname}
variants={pageTransition}
initial="initial"
animate="animate"
exit="exit"
className="min-h-dvh"
>
{children}
);
}
```
ה-prop mode="wait" מבטיח שהדף היוצא מסיים את אנימציה שלו לפני שהדף הנכנס מתחיל. אני שומר על מעברי עמודים מהירים (פחות מ-300ms בסה"כ) ועדינים - משתמשים צריכים להרגיש את הזרימה, לא לצפות באנימציה.
### אנימציות מחוות
Framer Motion מצטיין באנימציות מונעות מחוות. ה-props whileHover, whileTap ו-drag יוצרים אלמנטים אינטראקטיביים שמגיבים לקלט המשתמש ללא boilerplate.
```tsx
"use client";
import { motion } from "framer-motion";
export function InteractiveButton({ children, onClick }) {
return (
onClick={onClick}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="rounded-full bg-blue-600 px-8 py-3 font-medium text-white"
>
{children}
);
}
```
פיזיקת האביב על הכפתור יוצרת תחושה טבעית ומנטלת שהרבה יותר מספקת מאשר מעבר לינארי.
### עקרונות אנימציה שאני עוקב אחריהם
לאחר בניית עשרות ממשקים מונפשים, אלה הכללים שאני עוקב אחריהם ללא יוצא מן הכלל:
1. נמטו רק transform ו-opacity - אלה המאפיינים היחידים שלא מפעילים חישוב פריסה מחדש. לעולם אל תנמטו width, height, top, left או margin.
2. שמרו על משכים מתחת ל-200ms עבור משוב - לחיצות כפתור, מצבי hover, ומתגים צריכים להרגיש מיידיים.
3. השתמשו ב-300-600ms לחשיפות ומעברים - מעברי עמודים, אנימציות גלילה ואפקטי כניסה יכולים להיות איטיים יותר.
4. כבדו העדפות motion מופחת - חלק מהמשתמשים סובלים מהפרעות וסטיבולריות. תמיד ספקו חלופה עם motion מופחת.
5. נמטו עם מטרה - כל אנימציה צריכה לתקשר משהו: שינוי מצב, קשר מרחבי, או רמז לאינטראקטיביות.
6. סטאגרו, אל תסנכרנו - כשמספר אלמנטים מונפשים פנימה, סטאגרו את התזמון שלהם. אנימציות סימולטניות מרגישות מכניות; מדורגות מרגישות אורגניות.
### שיקולי ביצועים
Framer Motion מאוד מותאם, אבל אתם עדיין יכולים ליצור בעיות ביצועים אם לא זהירים:
- הימנעו מנמטת במהלך גלילה - השתמשו ב-
whileInViewעםonce: trueבמקום אנימציות מקושרות גלילה רציפות - השתמשו ב-
layoutאנימציות בחיסכון - ה-proplayoutמפעיל מדידות פריסה בכל פריים - הסירו רכיבים מונפשים - השתמשו ב-
AnimatePresenceלניקוי נכון של רכיבים - קבצו אנימציות קשורות - השתמשו ב-variants עם
staggerChildrenבמקום props שלdelayבודדים
### סיכום
Framer Motion הופך אנימציה ב-React מתהליך כואב ואימפרטיבי לתהליך דקלרטיבי וניתן להרכבה. ממשק ה-API של רכיב ה-motion מרגיש טבעי למפתחי React, variants מטפלים בתזמור מורכב בצורה אלגנטית, וברירות המחדל של ביצועים הגיוניות.
המפתח הוא ריסון. האנימציות הטובות ביותר הן אלה שמשתמשים מרגישים אבל לא מבחינים בהן בצורה מודעת. הן גורמות ליישום שלכם להרגיש רספונסיבי, מלוטש וחי ללא הסחת דעת מהתוכן.
רוצים לראות את האנימציות האלה בפעולה? עיינו באתר הזה - כל מעבר ואפקט hover משתמש בדפוסים המתוארים לעיל. יש שאלות? צרו קשר.