בפרקים 4-5 למדתם GSAP — מנוע אנימציה עוצמתי שעובד בכל מקום. אבל אם אתם עובדים ב-React (וב-2026 רוב ה-Vibe Coders עובדים ב-React דרך Next.js, Remix, או Vite), יש ספרייה שתוכננה בדיוק בשבילכם: Motion — שנולדה כ-Framer Motion ומ-2024 הפכה לספרייה עצמאית שתומכת ב-React, Vue, וגם vanilla JavaScript. Motion לא מחליפה את GSAP — היא ממלאת תפקיד אחר. היא שכבת אנימציה declarative שמשתלבת עם React כמו שרכיבים משתלבים — עם props, state, ו-lifecycle. במקום לכתוב gsap.to() בתוך useEffect ולדאוג ל-cleanup, כותבים <motion.div animate={{ opacity: 1 }}> וזה עובד. בפרק הזה תלמדו הכל: מ-motion.div בסיסי, דרך spring physics, variants, AnimatePresence, layout animations, gestures, scroll hooks — ועד טבלת ההחלטה GSAP vs Motion שתעזור לכם לבחור את הכלי הנכון.
בפרקים 4-5 בניתם hero section עם GSAP + ScrollTrigger — אנימציות imperative שאתם שולטים בכל שורה. בפרק הזה תבנו את אותו hero section מחדש ב-React עם Motion — ותראו את ההבדל הדרמטי בגישה. במקום gsap.from() ב-useEffect תכתבו <motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }}> ישירות ב-JSX. תוסיפו AnimatePresence לדפים שמשתנים, layout animations לכרטיסים שזזים, ו-useScroll לאפקטי scroll — הכל ב-React. בפרק 7 (Lenis) תלמדו לשלב smooth scroll עם Motion ליצירת חוויה שלמה.
| מונח (English) | תרגום | הגדרה |
|---|---|---|
| motion component | רכיב תנועה | רכיב Motion שמחליף אלמנט HTML רגיל — motion.div, motion.span, motion.button וכו'. מקבל props של אנימציה כמו animate, initial, exit |
| animate | מצב יעד | ה-prop הראשי של motion component — מגדיר את מצב היעד של האנימציה. למשל animate={{ opacity: 1, y: 0 }} מאנים לשקיפות מלאה ומיקום אפס |
| initial | מצב התחלתי | מגדיר את המצב ההתחלתי לפני האנימציה. initial={{ opacity: 0, y: 20 }} = האלמנט מתחיל שקוף ו-20px למטה |
| exit | מצב יציאה | מגדיר את האנימציה שמתרחשת כשהרכיב יוצא מה-DOM. דורש AnimatePresence כ-wrapper. מה ש-CSS ו-React לא יכולים לעשות לבד |
| variants | וריאנטים | אובייקט שמגדיר animation states בשם — למשל { hidden: { opacity: 0 }, visible: { opacity: 1 } }. מאפשר הפעלה לפי שם, propagation לילדים, ו-orchestration |
| AnimatePresence | נוכחות מונפשת | רכיב wrapper שמאפשר exit animations — כשרכיב ילד נמחק מה-DOM, AnimatePresence מעכב את המחיקה עד שאנימציית ה-exit מסתיימת |
| layoutId | מזהה פריסה | prop שמקשר בין שני motion components — כשרכיב עם layoutId מסוים נעלם ורכיב אחר עם אותו layoutId מופיע, Motion מאנים ביניהם אוטומטית. "magic move" |
| spring | קפיץ | סוג transition שמדמה פיזיקה של קפיץ — stiffness (קשיחות), damping (ריסון), mass (מסה). ברירת המחדל ב-Motion. יוצר תנועה טבעית עם overshoot |
| useScroll | הוק גלילה | React hook של Motion שמחזיר MotionValues של מיקום הגלילה — scrollY, scrollYProgress (0-1). משמש לאנימציות scroll ב-React |
| useTransform | הוק טרנספורם | React hook שממפה MotionValue לטווח אחר — למשל ממפה scrollYProgress (0-1) ל-opacity (1-0). מחבר בין scroll לאנימציה בלי re-render |
| gesture | מחוות | אינטראקציה של המשתמש — whileHover, whileTap, whileDrag, whileFocus. ב-Motion, כל gesture הוא prop ישיר על motion component |
| orchestration | תזמור | שליטה בסדר האנימציות של ילדים — staggerChildren (עיכוב בין ילדים), delayChildren (עיכוב כללי), when: "beforeChildren"/"afterChildren" (סדר הפעלה) |
Motion (שהייתה ידועה כ-Framer Motion עד 2024) היא ספריית אנימציה declarative שנולדה בתוך Framer — כלי עיצוב פרוטוטיפינג. מאט פרי (Matt Perry), המפתח הראשי, בנה אותה כדי לפתור בעיה ספציפית: אנימציות ב-React הן מסובכות. React הוא declarative — אתם מתארים מה אתם רוצים ו-React דואג ל-איך. אבל אנימציות הן inherently imperative — "הזז את האלמנט הזה מכאן לשם". הפער הזה יוצר קוד מכוער: useEffect עם refs, cleanup functions, התנגשויות עם re-renders. Motion פותרת את זה על ידי כך שהיא הופכת אנימציות ל-declarative — כמו React עצמה.
מ-Framer Motion ל-Motion: בסוף 2024, הספרייה עברה rebranding משמעותי. היא יצאה מתחת לכנפי Framer והפכה לפרויקט עצמאי בשם "Motion". השינוי לא היה רק בשם — Motion הרחיבה תמיכה ל-Vue וגם ל-vanilla JavaScript (בלי framework בכלל). ה-API נשאר כמעט זהה, אז כל מה שלמדתם על Framer Motion עדיין רלוונטי. הייבוא השתנה מ-"framer-motion" ל-"motion/react" (ל-React) או "motion" (vanilla). בקורס הזה נשתמש ב-"motion/react" — כי זה ה-import המודרני.
// הייבוא הישן (עדיין עובד אבל deprecated):
// import { motion } from "framer-motion";
// הייבוא המודרני (2024+):
import { motion } from "motion/react";
// ל-Vue:
// import { motion } from "motion/vue";
// ל-vanilla JavaScript (בלי framework):
// import { animate } from "motion";
למה Motion ולא CSS/GSAP ב-React? שלוש סיבות:
layout.npm install motionimport { motion } from "motion/react";<motion.div animate={{ scale: 1.2 }} transition={{ duration: 0.5 }}>Hello Motion</motion.div>אם אתם קוראים מדריכים ישנים (לפני 2024) או שה-AI מייצר קוד עם import { motion } from "framer-motion" — זה עדיין עובד, אבל ה-package הרשמי עכשיו הוא motion. אם אתם מתחילים פרויקט חדש, השתמשו ב-npm install motion ו-import מ-"motion/react". אם ה-AI מייצר את הייבוא הישן — פשוט שנו את שורת ה-import, ה-API זהה. שימו לב: ב-Bolt ו-Lovable, הפרומפט צריך לציין במפורש "use motion package (not framer-motion)" — אחרת ה-AI עלול להשתמש בגרסה הישנה.
הרעיון המרכזי של Motion הוא פשוט: במקום <div> רגיל, כתבו <motion.div>. זה אותו div בדיוק, אבל עם "על-כוחות" של אנימציה. כל אלמנט HTML יכול להפוך ל-motion component — motion.div, motion.span, motion.button, motion.h1, motion.img, motion.svg, motion.path ועוד. אפילו motion.li ו-motion.ul לרשימות.
import { motion } from "motion/react";
// דוגמה 1: fade in פשוט
<motion.div
initial={{ opacity: 0 }} // מצב התחלתי — שקוף
animate={{ opacity: 1 }} // מצב יעד — נראה
>
ברוכים הבאים
</motion.div>
// דוגמה 2: slide up + fade in
<motion.h1
initial={{ opacity: 0, y: 30 }} // מתחיל שקוף ו-30px למטה
animate={{ opacity: 1, y: 0 }} // מסתיים נראה ובמקום
transition={{ duration: 0.6 }} // 0.6 שניות
>
כותרת ראשית
</motion.h1>
// דוגמה 3: כמה properties ביחד
<motion.div
initial={{ opacity: 0, scale: 0.8, rotate: -10 }}
animate={{ opacity: 1, scale: 1, rotate: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
כרטיס מוצר
</motion.div>
מה קורה מאחורי הקלעים: כש-React מרנדר את ה-motion.div, Motion קורא את initial ומציב את האלמנט במצב ההתחלתי. אז היא מאנימת אוטומטית מ-initial ל-animate. אין צורך ב-useEffect או setTimeout — זה פשוט עובד. וכשה-props משתנים (למשל animate עובר מ-{ opacity: 1 } ל-{ opacity: 0 }), Motion מאנימת אוטומטית למצב החדש. זה ה-declarative magic — אתם מתארים את המצב, Motion דואגת לתנועה.
// transition פשוט — duration ו-ease
<motion.div
animate={{ x: 100 }}
transition={{
duration: 0.8,
ease: "easeInOut" // כמו CSS ease-in-out
}}
/>
// transition עם delay
<motion.div
animate={{ opacity: 1 }}
transition={{
duration: 0.5,
delay: 0.3 // מחכה 0.3 שניות לפני ההתחלה
}}
/>
// transition שונה לכל property
<motion.div
animate={{ opacity: 1, y: 0 }}
transition={{
opacity: { duration: 0.3 }, // opacity — 0.3 שניות
y: { duration: 0.5, ease: "easeOut" } // y — 0.5 שניות, easeOut
}}
/>
// repeat — אנימציה חוזרת
<motion.div
animate={{ rotate: 360 }}
transition={{
duration: 2,
repeat: Infinity, // חוזר לנצח
ease: "linear" // מהירות קבועה לסיבוב
}}
/>
easing ב-Motion: Motion תומך בכל ה-easing functions שלמדתם בפרק 3 — "easeIn", "easeOut", "easeInOut", "linear", ו-cubic bezier כמערך [0.42, 0, 0.58, 1]. אבל ברירת המחדל של Motion היא spring — לא ease! כשלא מציינים transition, Motion משתמשת ב-spring physics. זה אחד ההבדלים הגדולים מ-GSAP (ששם ברירת המחדל היא "power1.out"). בסעיף 6.3 נצלול לעומק ל-spring.
import { motion } from "motion/react";
import { useState } from "react";
function ToggleBox() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
<motion.div
animate={{
height: isOpen ? "auto" : 0, // נפתח/נסגר
opacity: isOpen ? 1 : 0
}}
transition={{ duration: 0.3 }}
style={{ overflow: "hidden" }}
>
<p>תוכן שנפתח ונסגר בצורה חלקה</p>
</motion.div>
</div>
);
}
// דוגמה 2: כפתור עם סטטוסים
function StatusButton() {
const [status, setStatus] = useState("idle");
const colors = {
idle: "#3B82F6", // כחול
loading: "#F59E0B", // צהוב
success: "#10B981", // ירוק
error: "#EF4444" // אדום
};
return (
<motion.button
animate={{
backgroundColor: colors[status],
scale: status === "loading" ? 0.95 : 1
}}
transition={{ duration: 0.3 }}
whileTap={{ scale: 0.9 }}
onClick={() => {
setStatus("loading");
setTimeout(() => setStatus("success"), 1500);
}}
>
{status === "idle" && "שלח"}
{status === "loading" && "שולח..."}
{status === "success" && "נשלח! ✓"}
</motion.button>
);
}
זה הכוח האמיתי של Motion — אנימציות שמגיבות ל-state. שימו לב: אין useEffect, אין refs, אין cleanup. כשה-state משתנה, React מרנדר מחדש, Motion רואה שה-animate prop השתנה, ומאנימת אוטומטית. זה הרבה יותר נקי מהאלטרנטיבה עם GSAP:
// אותו דבר ב-GSAP — שימו לב לכמות הקוד:
import { useEffect, useRef } from "react";
import gsap from "gsap";
function ToggleBoxGSAP() {
const [isOpen, setIsOpen] = useState(false);
const boxRef = useRef(null);
useEffect(() => {
if (isOpen) {
gsap.to(boxRef.current, { height: "auto", opacity: 1, duration: 0.3 });
} else {
gsap.to(boxRef.current, { height: 0, opacity: 0, duration: 0.3 });
}
}, [isOpen]);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
<div ref={boxRef} style={{ overflow: "hidden", opacity: 0, height: 0 }}>
<p>תוכן שנפתח ונסגר</p>
</div>
</div>
);
}
// יותר שורות, useEffect, ref, ותלות ב-isOpen בdependency array
initial={false} ל-motion.div — זה מדלג על אנימציית הכניסה הראשונה (שימושי כשהתוכן צריך להיות סגור בהתחלה בלי אנימציה)בפרק 3 למדתם על easing — עקומות שמגדירות מהירות לאורך אנימציה. למדתם גם על spring physics — סימולציה של קפיץ פיזיקלי. בפרק הזה נצלול לעומק, כי spring הוא ברירת המחדל של Motion. כשכותבים animate={{ x: 100 }} בלי transition — Motion משתמשת ב-spring. זה אומר שהאלמנט ינוע כמו שמחובר לקפיץ: יתאץ, יעבור את היעד (overshoot), יתנדנד, ויתייצב. זה מרגיש טבעי כי זו פיזיקה אמיתית — חוקי ניוטון, לא עקומת Bezier שרירותית.
// spring ברירת מחדל — מרגיש "חי" ותגובתי
<motion.div
animate={{ x: 200 }}
transition={{ type: "spring" }} // ברירת מחדל — לא חייבים לציין
/>
// spring קשיח — תנועה מהירה, overshoot קטן
<motion.div
animate={{ x: 200 }}
transition={{
type: "spring",
stiffness: 300, // קשיחות הקפיץ (ברירת מחדל: 100)
damping: 20 // ריסון — כמה מהר התנודה נעצרת (ברירת מחדל: 10)
}}
/>
// spring רך — תנועה איטית, תנודות רבות
<motion.div
animate={{ x: 200 }}
transition={{
type: "spring",
stiffness: 50, // קפיץ רך
damping: 5, // ריסון נמוך = הרבה תנודות
mass: 2 // מסה כפולה = תנועה כבדה יותר
}}
/>
// spring "בוגר" — הגיע ליעד בלי overshoot
<motion.div
animate={{ x: 200 }}
transition={{
type: "spring",
stiffness: 100,
damping: 20 // damping גבוה = בלי תנודה (critically damped)
}}
/>
| פרמטר | ברירת מחדל | נמוך | גבוה | משמעות בפרקטיקה |
|---|---|---|---|---|
| stiffness | 100 | 50 = קפיץ רך, איטי | 500 = קפיץ קשיח, מהיר | כמה חזק הקפיץ "מושך" לכיוון היעד. UI buttons: 200-400. Page transitions: 80-150 |
| damping | 10 | 5 = הרבה תנודות | 30 = כמעט בלי תנודה | כמה מהר התנודה נעצרת. Playful: 8-12. Professional: 15-25. No bounce: 25+ |
| mass | 1 | 0.5 = קל, תגובתי | 3 = כבד, עצלני | משפיע על ההתאצלות וזמן ההאטה. רוב הפעמים משאירים 1. גדול יותר = מרגיש "כבד" |
קשר לפרק 3: בפרק 3 הסברנו שב-spring אין duration — האנימציה נמשכת עד שהקפיץ מתייצב. ב-Motion, אם מציינים duration ביחד עם type: "spring", Motion מתאימה אוטומטית את stiffness ו-damping כדי שהאנימציה תסתיים בזמן שציינתם. זה shortcut שימושי:
// spring עם duration — Motion מחשבת stiffness/damping אוטומטית
<motion.div
animate={{ x: 200 }}
transition={{
type: "spring",
duration: 0.5, // האנימציה תסתיים בערך ב-0.5 שניות
bounce: 0.3 // כמה "קפיצה" (0 = בלי, 1 = הרבה)
}}
/>
// bounce — הדרך הקלה לשלוט ב-spring
// bounce: 0 = כמו ease — בלי overshoot
// bounce: 0.25 = קפיצה עדינה — מקצועי
// bounce: 0.5 = קפיצה בולטת — playful
// bounce: 0.8 = קפיצה מוגזמת — cartoon
Spring מתאים ל: כפתורים, כרטיסים, tooltips, dropdowns, modals — כל דבר אינטראקטיבי שהמשתמש מפעיל. ה-overshoot הקל מרגיש "חי" ותגובתי. Ease מתאים ל: page transitions, scroll animations, loading sequences, progress bars — תנועות ארוכות ומתוכננות שבהן overshoot יהיה מוזר. כלל אצבע: אם האנימציה מגיבה לאינטראקציה ישירה (click, hover, tap) — spring. אם היא חלק מ-sequence מתוכנן — ease.
Spring עם rotate יכול להרגיש מוזר — האלמנט "מתנדנד" ימינה-שמאלה כמו מטוטלת. לפעמים זה רצוי (אנימציה playful), אבל בדרך כלל עדיף ease לסיבובים. הפתרון: transition={{ rotate: { type: "tween", duration: 0.3, ease: "easeOut" }, default: { type: "spring" } }} — spring לכל ה-properties חוץ מ-rotate שמקבל ease.
עד עכשיו הגדרנו אנימציות inline — animate={{ opacity: 1 }}. זה עובד לאנימציות פשוטות, אבל כשיש כמה מצבים (hidden, visible, hovered, selected) ו-orchestration בין parent ל-children, צריך Variants. Variants הם אובייקט שמגדיר animation states לפי שם — כמו CSS classes, אבל לאנימציות.
import { motion } from "motion/react";
// הגדרת variants
const cardVariants = {
hidden: {
opacity: 0,
y: 30
},
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: "easeOut" }
},
hover: {
y: -5,
boxShadow: "0 10px 30px rgba(0,0,0,0.15)",
transition: { type: "spring", stiffness: 300, damping: 20 }
}
};
// שימוש ב-variants
<motion.div
variants={cardVariants}
initial="hidden" // שם ה-variant ההתחלתי
animate="visible" // שם ה-variant היעד
whileHover="hover" // שם ה-variant בזמן hover
className="card"
>
תוכן הכרטיס
</motion.div>
למה variants ולא inline? שלוש סיבות: (1) קריאות — "hidden" ו-"visible" קריאים יותר מ-{{ opacity: 0, y: 30 }}. (2) Reusability — מגדירים פעם אחת, משתמשים בכמה רכיבים. (3) Propagation — parent יכול להפעיל variant על כל הילדים אוטומטית. וזה הכוח האמיתי:
// parent variants עם orchestration
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15, // 0.15 שניות בין כל ילד
delayChildren: 0.3, // 0.3 שניות לפני הילד הראשון
when: "beforeChildren" // ה-parent מסיים לפני הילדים
}
}
};
// children variants — לא צריכים transition, ה-parent שולט
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
// שימוש — שימו לב: הילדים לא צריכים initial/animate!
function CardGrid({ items }) {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid"
>
{items.map((item) => (
<motion.div
key={item.id}
variants={itemVariants} // רק variants — בלי initial/animate
className="card"
>
{item.title}
</motion.div>
))}
</motion.div>
);
}
// התוצאה: container נכנס → 0.3 שניות המתנה → כרטיסים נכנסים
// אחד אחרי השני עם 0.15 שניות ביניהם. stagger אוטומטי!
Propagation עובד אוטומטית: כשה-parent עובר ל-variant "visible", כל ילד עם variants שמכיל "visible" יעבור גם הוא — אוטומטית, בלי שום קוד נוסף. זה עובד לכל עומק — ילדים של ילדים גם מקבלים את ה-variant. וה-orchestration (staggerChildren, delayChildren) מוגדר ב-parent ומשפיע על כל הילדים.
// staggerChildren — עיכוב בין ילד לילד
{
visible: {
transition: {
staggerChildren: 0.1, // 0.1s בין כל ילד
staggerDirection: -1, // הפוך — מהאחרון לראשון
}
}
}
// delayChildren — עיכוב לפני כל הילדים
{
visible: {
transition: {
delayChildren: 0.5, // חצי שנייה לפני הילד הראשון
staggerChildren: 0.1
}
}
}
// when — סדר ה-parent מול הילדים
{
visible: {
opacity: 1,
transition: {
when: "beforeChildren", // parent מסיים, אז ילדים מתחילים
staggerChildren: 0.1
}
}
}
// אפשרויות when:
// "beforeChildren" — parent קודם, אז ילדים
// "afterChildren" — ילדים קודם, אז parent
// לא מוגדר — parent וילדים ביחד (ברירת מחדל)
// variants שמקבלים פרמטר
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: (index) => ({ // פונקציה שמקבלת custom
opacity: 1,
y: 0,
transition: {
delay: index * 0.1 // delay לפי index
}
})
};
// שימוש עם custom prop
{items.map((item, index) => (
<motion.div
key={item.id}
variants={itemVariants}
custom={index} // מעביר את ה-index כ-custom
initial="hidden"
animate="visible"
/>
))}
custom הוא prop שמעביר ערך דינמי ל-variant function. זה שימושי כש-staggerChildren לא מספיק — למשל כשרוצים delay שונה לפי מיקום בגריד (row ו-column), לא רק לפי סדר.
קריטריון הצלחה: התפריט נפתח עם stagger חלק, נסגר עם stagger הפוך, וכל פריט מגיב ל-hover.
בעיה מרכזית באנימציות React: כש-React מסיר רכיב מה-DOM (תנאי שמתקיים, רשימה שמתקצרת, דף שמשתנה), הרכיב פשוט נעלם. אין "exit animation". ב-CSS רגיל אין פתרון — :not(:defined) לא עובד. ב-GSAP צריך ניהול ידני (לעכב את ה-removal, להריץ אנימציה, ואז להסיר). AnimatePresence של Motion פותר את זה בצורה אלגנטית:
import { motion, AnimatePresence } from "motion/react";
import { useState } from "react";
function Notification() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
<AnimatePresence>
{isVisible && (
<motion.div
key="notification" // key חיוני!
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }} // ← exit animation!
transition={{ duration: 0.3 }}
className="notification"
>
הודעה חשובה!
</motion.div>
)}
</AnimatePresence>
</div>
);
}
// כש-isVisible הופך ל-false, במקום שהרכיב נעלם מיידית —
// הוא מאנים ל-exit (fade out + slide up) ורק אז נמחק מה-DOM
איך זה עובד: AnimatePresence עוקב אחרי הילדים שלו (לפי key). כשילד "נעלם" (התנאי הופך ל-false), AnimatePresence לא מסיר אותו מיד — הוא מעכב את ההסרה, מפעיל את אנימציית ה-exit, ורק כשהאנימציה מסתיימת — מוחק מה-DOM. זה בדיוק מה שלא אפשרי בלעדיו.
// mode="wait" — מחכה שהישן ייצא לפני שהחדש נכנס
<AnimatePresence mode="wait">
<motion.div
key={currentPage} // key שונה = רכיב שונה
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3 }}
>
{pages[currentPage]}
</motion.div>
</AnimatePresence>
// mode="popLayout" — החדש נכנס מיד, הישן "נדחף" החוצה
<AnimatePresence mode="popLayout">
...
</AnimatePresence>
// mode="sync" (ברירת מחדל) — הישן יוצא והחדש נכנס ביחד
<AnimatePresence mode="sync">
...
</AnimatePresence>
mode="wait" הוא הכי נפוץ — במיוחד ל-page transitions. הדף הישן עושה fade-out, ורק אחרי שנעלם הדף החדש עושה fade-in. בלי mode="wait", שני הדפים יהיו על המסך בו-זמנית (הישן יוצא והחדש נכנס), מה שנראה לפעמים מוזר.
AnimatePresence מזהה שינויים על פי key. אם key לא משתנה — AnimatePresence לא יזהה שהרכיב "הוחלף". שתי טעויות נפוצות: (1) שוכחים key לגמרי — exit לא עובד. (2) משתמשים ב-index כ-key ברשימה — כשפריט נמחק מהאמצע, React מבלבל את ה-keys ו-AnimatePresence מאנים את הפריט הלא נכון. תמיד השתמשו ב-unique id כ-key — לא index.
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "למד Motion" },
{ id: 2, text: "בנה פרויקט" },
{ id: 3, text: "deploy!" }
]);
const removeTodo = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
return (
<ul>
<AnimatePresence>
{todos.map(todo => (
<motion.li
key={todo.id} // id ייחודי — לא index!
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
style={{ overflow: "hidden" }}
>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>×</button>
</motion.li>
))}
</AnimatePresence>
</ul>
);
}
// כשמוחקים todo, הוא לא נעלם מיד — הוא מתכווץ ומתפוגג בצורה חלקה
Layout animations הן אולי הפיצ'ר הכי מרשים של Motion — ואחד שאין לו מקבילה קלה ב-GSAP או CSS. הרעיון: כש-DOM משתנה (אלמנט עובר מיקום, רשימה מסתדרת מחדש, כרטיס מתרחב), Motion מזהה את השינוי ומאנימת אוטומטית את המעבר. הטכניקה מאחורי הקלעים היא FLIP (First, Last, Invert, Play) — אבל Motion עושה את זה אוטומטית.
// כפתור toggle שמשנה justify-content
function LayoutToggle() {
const [isRight, setIsRight] = useState(false);
return (
<div
style={{
display: "flex",
justifyContent: isRight ? "flex-end" : "flex-start",
padding: 20,
background: "#eee",
borderRadius: 12,
cursor: "pointer"
}}
onClick={() => setIsRight(!isRight)}
>
<motion.div
layout // ← רק המילה layout!
style={{
width: 60,
height: 60,
borderRadius: 30,
background: "#3B82F6"
}}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</div>
);
}
// כשלוחצים, ה-justify-content משתנה ← ה-ball צריך לזוז.
// בלי layout — הוא "קופץ" מיידית. עם layout — הוא מחליק בצורה חלקה!
layout prop אומר ל-Motion: "תעקוב אחרי המיקום שלי ב-DOM. אם הוא משתנה — תאנים אוטומטית". זה עובד לכל שינוי CSS — position, size, order, flex, grid. אין צורך לחשב coordinates ידנית. פשוט מוסיפים layout ו-Motion עושה את השאר.
// Gallery עם expand — כרטיס "עף" למיקום חדש
function ImageGallery() {
const [selectedId, setSelectedId] = useState(null);
return (
<div>
{/* Grid של thumbnails */}
<div className="grid">
{images.map(img => (
<motion.div
key={img.id}
layoutId={`card-${img.id}`} // ← layoutId ייחודי
onClick={() => setSelectedId(img.id)}
className="thumbnail"
style={{ cursor: "pointer" }}
>
<motion.img
layoutId={`img-${img.id}`} // layoutId גם לתמונה
src={img.src}
/>
</motion.div>
))}
</div>
{/* כרטיס מורחב */}
<AnimatePresence>
{selectedId && (
<motion.div
layoutId={`card-${selectedId}`} // אותו layoutId!
className="expanded-card"
onClick={() => setSelectedId(null)}
>
<motion.img
layoutId={`img-${selectedId}`}
src={images.find(i => i.id === selectedId).src}
/>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
תיאור מורחב של התמונה
</motion.p>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
// כשלוחצים על thumbnail — הכרטיס "עף" בצורה חלקה ממיקום הגריד
// למיקום ה-expanded. לחיצה שנייה — "עף" חזרה. Magic Move!
איך layoutId עובד: Motion מחפש שני רכיבים עם אותו layoutId. כשאחד נעלם ואחד מופיע (או שניהם קיימים אבל במיקומים שונים), Motion מחשב FLIP ומאנים ביניהם. זה עובד גם בין רכיבי React שונים לגמרי — כל עוד ל-layoutId אותו ערך. זו הסיבה שזה נקרא "magic move" — נראה כאילו אלמנט פיזי עובר ממקום למקום, גם אם ב-DOM אלה שני אלמנטים שונים.
קריטריון הצלחה: ה-underline "מחליק" בין tabs בצורה חלקה (לא קופץ), והתוכן מתחלף עם exit animation.
אנימציות gesture הן אנימציות שקורות בתגובה ישירה לפעולת המשתמש — hover, tap (click), drag, focus. ב-CSS יש :hover ו-:active, אבל הם מוגבלים ל-CSS properties ואין שליטה ב-transition type (אין spring, אין orchestration). ב-Motion, gestures הם props ישירים על motion components — וכל עולם האנימציות זמין:
// whileHover — אנימציה בזמן hover
<motion.button
whileHover={{
scale: 1.05,
boxShadow: "0 5px 15px rgba(0,0,0,0.2)"
}}
whileTap={{ scale: 0.95 }} // בזמן לחיצה — "נדחס"
transition={{ type: "spring", stiffness: 400, damping: 15 }}
>
לחץ כאן
</motion.button>
// whileFocus — לאלמנטים focusable (input, button)
<motion.input
whileFocus={{
scale: 1.02,
borderColor: "#3B82F6"
}}
transition={{ duration: 0.2 }}
type="text"
placeholder="הקלד כאן..."
/>
// שילוב — כרטיס אינטראקטיבי
<motion.div
whileHover={{ y: -5, boxShadow: "0 10px 30px rgba(0,0,0,0.15)" }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="card"
>
<h3>כרטיס מוצר</h3>
<p>תיאור המוצר</p>
</motion.div>
// drag בסיסי — גרירה חופשית
<motion.div
drag // מפעיל גרירה
whileDrag={{ scale: 1.1, boxShadow: "0 15px 40px rgba(0,0,0,0.3)" }}
dragConstraints={{ top: -100, bottom: 100, left: -100, right: 100 }}
dragElastic={0.2} // כמה "נמתח" מעבר לגבולות
dragTransition={{ bounceStiffness: 500, bounceDamping: 20 }}
style={{ cursor: "grab", width: 100, height: 100, background: "#3B82F6", borderRadius: 12 }}
/>
// drag רק בציר אחד
<motion.div
drag="x" // רק אופקית
dragConstraints={{ left: 0, right: 300 }}
whileDrag={{ scale: 1.05 }}
style={{ width: 60, height: 60, borderRadius: 30, background: "#10B981" }}
/>
// drag עם snap back
<motion.div
drag
dragConstraints={{ top: 0, bottom: 0, left: 0, right: 0 }} // snap back למקום
dragElastic={0.5} // נמתח 50% ואז חוזר
whileDrag={{ scale: 1.1 }}
/>
// drag עם dragSnapToOrigin
<motion.div
drag
dragSnapToOrigin // חוזר למקום המקורי אחרי שחרור
whileDrag={{ scale: 1.2, rotate: 5 }}
/>
dragConstraints מגביל את טווח הגרירה. אפשר להעביר ערכים בפיקסלים (כמו למעלה) או ref לאלמנט אב — והגרירה תוגבל לגבולות האלמנט:
import { useRef } from "react";
function DragInBox() {
const constraintsRef = useRef(null);
return (
<div ref={constraintsRef} style={{ width: 400, height: 300, background: "#f0f0f0" }}>
<motion.div
drag
dragConstraints={constraintsRef} // מוגבל לתוך ה-div האב
whileDrag={{ scale: 1.1 }}
style={{ width: 60, height: 60, borderRadius: 12, background: "#8B5CF6" }}
/>
</div>
);
}
// events שמופעלים ב-gestures
<motion.div
drag="x"
onDragStart={(event, info) => console.log("התחיל גרירה", info.point)}
onDrag={(event, info) => console.log("גורר", info.offset.x)}
onDragEnd={(event, info) => {
// info.velocity.x — מהירות הגרירה בסיום
if (info.velocity.x > 500) {
// swipe right — אפשר למשל למחוק
console.log("Swipe right!");
}
}}
onHoverStart={() => console.log("hover started")}
onHoverEnd={() => console.log("hover ended")}
onTap={(event, info) => console.log("tapped at", info.point)}
/>
ה-info object מכיל מידע שימושי: point (מיקום הנוכחי), offset (כמה זזנו מנקודת ההתחלה), velocity (מהירות). שימושי לבניית swipe gestures, dismiss actions, ו-drag-to-delete.
בפרק 5 למדתם ScrollTrigger — הפתרון של GSAP לאנימציות scroll. Motion מציעה גישה אחרת עם שני hooks: useScroll (קורא את מיקום הגלילה) ו-useTransform (ממפה ערכים). בואו נראה את שניהם, ואז נשווה ל-ScrollTrigger.
import { motion, useScroll, useTransform } from "motion/react";
function ScrollProgress() {
// useScroll מחזיר MotionValues:
const { scrollY, scrollYProgress } = useScroll();
// scrollY — מיקום גלילה בפיקסלים (0, 100, 500...)
// scrollYProgress — התקדמות 0-1 (0 = למעלה, 1 = למטה)
return (
<motion.div
className="progress-bar"
style={{
scaleX: scrollYProgress, // רוחב לפי התקדמות הגלילה
transformOrigin: "left",
position: "fixed",
top: 0,
left: 0,
right: 0,
height: 4,
background: "#3B82F6",
zIndex: 999
}}
/>
);
}
// progress bar שגדל ככל שגוללים למטה — בלי GSAP, בלי ScrollTrigger
MotionValue — הסוד מאחורי הביצועים: scrollYProgress הוא MotionValue — ערך מיוחד של Motion שמתעדכן בלי לגרום ל-React re-render. כשגוללים, scrollYProgress משתנה מ-0 ל-1, וה-progress bar מתעדכן — אבל React לא מרנדר מחדש את הרכיב. זו הסיבה שאנימציות scroll ב-Motion חלקות — הן עוקפות את ה-React reconciliation.
function ParallaxHero() {
const { scrollYProgress } = useScroll();
// מיפוי: כשגוללים 0-100%, ה-y נע 0 עד -150
const y = useTransform(scrollYProgress, [0, 1], [0, -150]);
// מיפוי: כשגוללים 0-50%, opacity יורד מ-1 ל-0
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
// מיפוי: כשגוללים 0-30%, ה-scale יורד מ-1 ל-0.8
const scale = useTransform(scrollYProgress, [0, 0.3], [1, 0.8]);
return (
<motion.div
className="hero"
style={{
y, // motion MotionValues כ-style
opacity,
scale,
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
>
<h1>כותרת Hero עם Parallax</h1>
</motion.div>
);
}
// כשגוללים: ה-hero זז למעלה, מתפוגג, ומתכווץ — parallax מלא!
import { useRef } from "react";
import { motion, useScroll, useTransform } from "motion/react";
function SectionReveal() {
const sectionRef = useRef(null);
// עוקב אחרי ה-section, לא אחרי כל הדף
const { scrollYProgress } = useScroll({
target: sectionRef, // עוקב אחרי אלמנט ספציפי
offset: ["start end", "end start"] // start=כניסה, end=יציאה
});
// opacity: 0 → 1 כשנכנס, 1 → 0 כשיוצא
const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [0, 1, 1, 0]);
// y: 50 → 0 כשנכנס, 0 → -50 כשיוצא
const y = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], [50, 0, 0, -50]);
return (
<motion.section
ref={sectionRef}
style={{ opacity, y }}
>
<h2>Section שנכנס ויוצא בצורה חלקה</h2>
<p>תוכן ה-section...</p>
</motion.section>
);
}
// offset מסביר "מתי מתחיל" ו"מתי מסתיים":
// ["start end", "end start"] = מתחיל כשתחילת האלמנט מגיע לתחתית ה-viewport
// מסתיים כשסוף האלמנט עובר את ראש ה-viewport
// זה מקביל ל-ScrollTrigger עם start: "top bottom" ו-end: "bottom top"
function ComplexParallax() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
});
// שכבות parallax — כל שכבה זזה במהירות שונה
const bgY = useTransform(scrollYProgress, [0, 1], ["-10%", "10%"]);
const contentY = useTransform(scrollYProgress, [0, 1], ["0%", "0%"]);
const floatY = useTransform(scrollYProgress, [0, 1], ["20%", "-20%"]);
// סיבוב שמגיב ל-scroll
const rotate = useTransform(scrollYProgress, [0, 1], [0, 360]);
// צבע רקע שמשתנה עם הגלילה
const backgroundColor = useTransform(
scrollYProgress,
[0, 0.5, 1],
["#1a1a2e", "#16213e", "#0f3460"]
);
return (
<motion.section ref={ref} style={{ backgroundColor, position: "relative" }}>
<motion.div className="bg-layer" style={{ y: bgY }} />
<motion.div className="content-layer" style={{ y: contentY }}>
<h2>תוכן</h2>
</motion.div>
<motion.div className="float-layer" style={{ y: floatY, rotate }} />
</motion.section>
);
}
useScroll משתמש ב-window.scrollY שלא קיים בצד שרת (SSR). ב-Next.js, עטפו את הרכיב ב-"use client" (כבר ברור ב-Next.js 13+). אם אתם רואים "window is not defined" — זו הסיבה. פתרון: הוסיפו "use client" בראש הקובץ, או עטפו ב-dynamic import עם ssr: false. AI כמו Bolt ו-v0 לפעמים שוכחים את זה — בדקו.
אחרי שלמדתם גם GSAP (פרקים 4-5) וגם Motion (הפרק הזה), השאלה הגדולה: מתי מה? התשובה הקצרה: Motion ל-React UI, GSAP לכל השאר. אבל בואו נפרט:
| קריטריון | GSAP | Motion |
|---|---|---|
| Framework | כל דבר — vanilla JS, React, Vue, Svelte, WordPress, static HTML | React (מלא), Vue (בטא), vanilla (חלקי) |
| גישה | Imperative — gsap.to(), gsap.timeline() | Declarative — props על JSX |
| Exit animations | ניהול ידני של DOM lifecycle | AnimatePresence — שורה אחת |
| Layout animations | FLIP plugin (ידני) | layout prop / layoutId — אוטומטי |
| Scroll animations | ScrollTrigger — עוצמתי מאוד (pin, snap, batch, horizontal) | useScroll + useTransform — בסיסי אבל חלק |
| Spring physics | אין מובנה (אפשר CustomEase) | ברירת מחדל — stiffness, damping, mass |
| Timelines | gsap.timeline() — עוצמתי, labels, position parameter | אין timeline מובנה. Variants עם orchestration כתחליף |
| SVG animations | מצוין — MorphSVG, DrawSVG, MotionPath | בסיסי — pathLength, motion.path |
| Text animations | SplitText — חלוקה לתווים/מילים/שורות | אין מובנה — צריך ספריית צד שלישי |
| ביצועים | מהיר מאוד — מותאם ל-60fps | טוב — MotionValues עוקפים re-renders |
| Bundle size | ~25KB min (core) + ~10KB (ScrollTrigger) | ~18KB min (tree-shakeable) |
| AI code generation | AI יודע GSAP טוב — הרבה דוגמאות באינטרנט | AI מצוין עם Motion — קוד declarative קל לייצר |
| עקומת למידה | בינונית — צריך ללמוד API גדול | קלה-בינונית — אם מכירים React, Motion טבעי |
| רישיון | חינם לרוב. פלאגינים מתקדמים (MorphSVG, DrawSVG) = Club | חינם לגמרי, MIT license |
| השאלה | אם כן | אם לא |
|---|---|---|
| האם הפרויקט ב-React/Next.js? | ↓ המשך (Motion אפשרי) | → GSAP (Motion לא רלוונטי) |
| צריך exit animations (modals, pages, lists)? | → Motion (AnimatePresence) | ↓ המשך |
| צריך layout animations (magic move, reorder)? | → Motion (layout/layoutId) | ↓ המשך |
| צריך scroll מתקדם (pin, snap, horizontal)? | → GSAP ScrollTrigger | ↓ המשך |
| צריך timelines מורכבים (sequence, labels)? | → GSAP | ↓ המשך |
| צריך SVG מתקדם (morph, draw, path)? | → GSAP | ↓ המשך |
| צריך text splitting (SplitText)? | → GSAP | ↓ המשך |
| אנימציות UI רגילות (hover, tap, entrance)? | → Motion (פשוט יותר ב-React) | → GSAP |
התשובה הפרקטית ל-Vibe Coders: בפרויקט React/Next.js, השתמשו ב-Motion כברירת מחדל לכל אנימציות UI — hover effects, entrance animations, exit animations, layout changes, gestures. כשצריך משהו ש-Motion לא יכול — scroll מתקדם (pin, snap), timelines מורכבים, SVG morph, text splitting — הוסיפו GSAP בנוסף. הם לא מתנגשים. אפשר להשתמש בשניהם באותו פרויקט. הכלל: Motion ל-UI layer, GSAP ל-wow layer.
Motion ו-AI הם שילוב מושלם. הקוד של Motion הוא declarative — קל לתאר בפרומפט ו-AI מייצר קוד נקי. "add a fade-in animation" עם Motion זה שורה אחת. אבל פרומפט מדויק יותר ייתן תוצאה מקצועית יותר. הנה 5 פרומפטים שעובדים:
Add entrance animations to this React page using the motion package
(import from "motion/react", NOT "framer-motion").
For each section heading: motion.h2 with initial={{ opacity: 0, y: 20 }}
and animate={{ opacity: 1, y: 0 }}.
For card grids: wrap the container in motion.div with variants that include
staggerChildren: 0.1. Each card should be motion.div with variants for
hidden (opacity: 0, y: 30) and visible (opacity: 1, y: 0).
Use whileInView instead of animate for below-the-fold content:
whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, amount: 0.3 }}
Transition: type "spring" with stiffness 100 and damping 15.
Do NOT use useEffect for any animations — everything should be declarative props.
Create a Modal component in React using motion from "motion/react".
The modal should have:
1. An overlay (motion.div) that fades in/out: initial/exit opacity 0,
animate opacity 1. Background: rgba(0,0,0,0.5). onClick closes modal.
2. A modal panel (motion.div) that scales and fades:
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
3. Wrap both in AnimatePresence with mode="wait"
4. Transition: type "spring", stiffness 300, damping 25
The parent component should render:
<AnimatePresence>
{isOpen && <Modal onClose={() => setIsOpen(false)} />}
</AnimatePresence>
Important: the modal must have a unique key prop.
Stop body scroll when modal is open (overflow: hidden on body).
Add onKeyDown handler for Escape key to close.
Add page transition animations to this Next.js App Router project
using motion from "motion/react".
Create a PageTransition wrapper component:
- Wraps {children} in AnimatePresence mode="wait"
- Each page content wrapped in motion.div with:
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
- Key should be based on pathname (use usePathname from next/navigation)
Place the PageTransition in the root layout.tsx, wrapping {children}.
Mark it as "use client" since it uses hooks.
Test: navigating between pages should show a smooth crossfade.
Do NOT use framer-motion package — use motion package with "motion/react" import.
Build a drag-to-reorder list component in React using motion
from "motion/react" with the Reorder module.
Import { Reorder } from "motion/react".
Use Reorder.Group and Reorder.Item components.
The list should:
1. Display 5 items with text and a drag handle icon
2. Each Reorder.Item has layout prop for smooth reordering animation
3. whileDrag: scale 1.02, boxShadow "0 5px 15px rgba(0,0,0,0.15)"
4. transition: type "spring", stiffness 300, damping 25
5. State managed with useState array, updated via onReorder callback
Add a subtle spring animation when items settle into new positions.
The drag handle should use cursor: grab (cursor: grabbing while dragging).
Style each item as a card with padding, border-radius, and subtle border.
Create a scroll-driven landing page in React using motion
from "motion/react". Use useScroll and useTransform hooks.
Sections:
1. Hero: Fixed progress bar at top using scrollYProgress → scaleX.
Hero text fades out and moves up as user scrolls (useTransform).
2. Features: 3 cards with whileInView entrance animation.
Use variants with staggerChildren: 0.15 on the container.
3. Showcase: Full-width image with parallax using useScroll with
target ref and useTransform for y movement.
4. CTA: Scale up animation using scroll progress of that section.
Each section should use its own useScroll with target ref and
offset: ["start end", "end start"].
Do NOT use GSAP or ScrollTrigger — pure Motion hooks only.
Import everything from "motion/react".
Add "use client" at the top for Next.js compatibility.
קריטריון הצלחה: ה-modal נפתח עם animation, נסגר עם exit animation, ויש overlay שמתפוגג. אם ה-AI הוציא "framer-motion" — שנו ידנית ל-"motion/react".
בסעיפים הקודמים למדתם את כל הכלים. עכשיו נרכיב אותם ל-3 patterns מקצועיים שתשתמשו בהם שוב ושוב בפרויקטים אמיתיים. כל pattern הוא production-ready — אפשר להעתיק ולהתאים.
import { motion, AnimatePresence } from "motion/react";
import { useEffect } from "react";
// Overlay variants
const overlayVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
};
// Modal variants עם stagger לילדים
const modalVariants = {
hidden: {
opacity: 0,
scale: 0.95,
y: 20
},
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 25,
staggerChildren: 0.1,
delayChildren: 0.1
}
},
exit: {
opacity: 0,
scale: 0.95,
y: 20,
transition: { duration: 0.2 }
}
};
// Content item variants (title, body, buttons)
const itemVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 }
};
function Modal({ isOpen, onClose, title, children }) {
// Escape key
useEffect(() => {
const handleEsc = (e) => e.key === "Escape" && onClose();
window.addEventListener("keydown", handleEsc);
return () => window.removeEventListener("keydown", handleEsc);
}, [onClose]);
// Lock body scroll
useEffect(() => {
if (isOpen) document.body.style.overflow = "hidden";
return () => { document.body.style.overflow = ""; };
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="modal-overlay"
variants={overlayVariants}
initial="hidden"
animate="visible"
exit="hidden"
onClick={onClose}
style={{
position: "fixed", inset: 0, zIndex: 1000,
background: "rgba(0,0,0,0.5)",
display: "flex", alignItems: "center", justifyContent: "center"
}}
>
<motion.div
key="modal-panel"
variants={modalVariants}
initial="hidden"
animate="visible"
exit="exit"
onClick={(e) => e.stopPropagation()}
style={{
background: "white", borderRadius: 16, padding: 32,
maxWidth: 500, width: "90%", maxHeight: "80vh", overflowY: "auto"
}}
>
<motion.h2 variants={itemVariants}>{title}</motion.h2>
<motion.div variants={itemVariants}>{children}</motion.div>
<motion.button
variants={itemVariants}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
style={{ marginTop: 20, padding: "10px 24px", borderRadius: 8 }}
>
סגור
</motion.button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
// app/components/PageTransition.jsx
"use client";
import { motion, AnimatePresence } from "motion/react";
import { usePathname } from "next/navigation";
const pageVariants = {
initial: { opacity: 0, y: 8 },
enter: {
opacity: 1,
y: 0,
transition: { duration: 0.3, ease: "easeOut" }
},
exit: {
opacity: 0,
y: -8,
transition: { duration: 0.2, ease: "easeIn" }
}
};
export default function PageTransition({ children }) {
const pathname = usePathname();
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
variants={pageVariants}
initial="initial"
animate="enter"
exit="exit"
>
{children}
</motion.div>
</AnimatePresence>
);
}
// app/layout.tsx — שימוש
// import PageTransition from "./components/PageTransition";
//
// export default function RootLayout({ children }) {
// return (
// <html>
// <body>
// <Navbar />
// <PageTransition>{children}</PageTransition>
// </body>
// </html>
// );
// }
import { Reorder } from "motion/react";
import { useState } from "react";
const initialItems = [
{ id: "1", title: "למד Motion", done: false },
{ id: "2", title: "בנה modal", done: true },
{ id: "3", title: "הוסף page transitions", done: false },
{ id: "4", title: "תרגל layout animations", done: false },
{ id: "5", title: "deploy!", done: false }
];
function ReorderableList() {
const [items, setItems] = useState(initialItems);
return (
<Reorder.Group
axis="y"
values={items}
onReorder={setItems}
style={{ listStyle: "none", padding: 0 }}
>
{items.map(item => (
<Reorder.Item
key={item.id}
value={item}
whileDrag={{
scale: 1.03,
boxShadow: "0 8px 25px rgba(0,0,0,0.15)",
cursor: "grabbing"
}}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
style={{
padding: "16px 20px",
marginBottom: 8,
background: "white",
borderRadius: 12,
border: "1px solid #e2e8f0",
cursor: "grab",
display: "flex",
alignItems: "center",
gap: 12
}}
>
<span style={{ fontSize: 20 }}>☰</span>
<span style={{
textDecoration: item.done ? "line-through" : "none",
opacity: item.done ? 0.5 : 1
}}>
{item.title}
</span>
</Reorder.Item>
))}
</Reorder.Group>
);
}
// גוררים item — הוא "צף" עם shadow. משחררים — הרשימה מתאנמת
// למיקום החדש עם spring. הכל בשורות בודדות של קוד.
(1) initial={false} שנשכח: כשהדף נטען, כל motion component מריץ את animate מ-initial. אם יש 50 כרטיסים עם initial={{ opacity: 0 }}, הם כולם יעשו fade-in יחד — שנראה מוזר. פתרון: השתמשו ב-whileInView לרכיבים below-the-fold, או initial={false} לרכיבים שצריכים להופיע מיד. (2) layoutId כפול: אם שני רכיבים עם אותו layoutId נמצאים ב-DOM בו-זמנית — Motion מתבלבל ואנימציה מוזרה. כל layoutId חייב להיות unique בכל רגע. (3) Bundle size: Motion היא tree-shakeable — אם מייבאים רק { motion } בלי AnimatePresence, useScroll, Reorder — ה-bundle קטן יותר. אל תייבאו * כי זה מנפח.
whileInView={{ opacity: 1, y: 0 }}initial={{ opacity: 0, y: 30 }}viewport={{ once: true, amount: 0.3 }}// prefers-reduced-motion ב-Motion
import { useReducedMotion } from "motion/react";
function MyComponent() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
animate={{ x: 100 }}
transition={shouldReduceMotion
? { duration: 0 } // בלי אנימציה
: { type: "spring", stiffness: 200 } // אנימציה רגילה
}
/>
);
}
// או גלובלי — ב-CSS:
// @media (prefers-reduced-motion: reduce) {
// *, *::before, *::after {
// animation-duration: 0.01ms !important;
// transition-duration: 0.01ms !important;
// }
// }
| שלב | פעולה | זמן |
|---|---|---|
| 1. Setup | npm install motion. ייבוא מ-"motion/react". בדקו שמייבאים את ה-package הנכון | 2 דקות |
| 2. Entrance | הוסיפו initial + animate (או whileInView) לרכיבים מרכזיים. התחילו מ-hero ו-headings | 15 דקות |
| 3. Interactions | הוסיפו whileHover + whileTap לכפתורים ולכרטיסים. בחרו spring או ease לפי הסגנון | 10 דקות |
| 4. Variants | ארגנו אנימציות חוזרות ל-variants. הוסיפו stagger ל-grids ורשימות | 15 דקות |
| 5. Exit/Layout | הוסיפו AnimatePresence ל-modals, notifications, page transitions. layout ל-lists ו-grids דינמיים | 15 דקות |
| 6. Scroll | אם צריך scroll animations — הוסיפו useScroll + useTransform. Progress bar, parallax, fade-in | 10 דקות |
| 7. Test | בדקו: exit animations עובדים? layout smooth? mobile touch? prefers-reduced-motion? | 10 דקות |
import { motion } from "motion/react"עכשיו אתם יודעים שתי ספריות אנימציה: GSAP (imperative, עוצמתי, universal) ו-Motion (declarative, React-native, elegant). בפרק 7 תלמדו Lenis — ספריית smooth scroll שמשנה את כל חוויית הגלילה. Lenis לא מחליף את ScrollTrigger או useScroll — הוא פועל מתחת להם, ומשפר את הגלילה עצמה. תלמדו איך Lenis עובד, איך לשלב אותו עם GSAP וגם עם Motion, ולמה כל אתר premium משתמש ב-smooth scroll. אחרי Lenis, תהיה לכם שליטה מלאה על כל שכבת האנימציה — מ-CSS ועד scroll experience שלם.