Study Guide: Framer Motion
ทำไมต้อง Animation?
Section titled “ทำไมต้อง Animation?”Animation ที่ดีทำให้ UI รู้สึก มีชีวิต — ช่วยบอก user ว่าอะไรเปลี่ยน, อะไรสำคัญ, อะไรกดได้ ในฐานะ Designer ที่เขียนโค้ดได้ คุณจะสร้าง animation ที่ สวยจริง ไม่ใช่แค่ใช้งานได้
เส้นทาง: จาก CSS → Framer Motion
Section titled “เส้นทาง: จาก CSS → Framer Motion”Part 1: CSS Animation พื้นฐาน (ไม่ต้องใช้ library)
Section titled “Part 1: CSS Animation พื้นฐาน (ไม่ต้องใช้ library)”ก่อนใช้ Framer Motion ต้องเข้าใจ CSS animation ก่อน — เพราะ Framer Motion สร้างมาบน concept เดียวกัน
ลองเล่นก่อน!
Section titled “ลองเล่นก่อน!”ลองกดปุ่ม, hover, และ replay เพื่อดู CSS animation แต่ละแบบ — ทุกอย่างเป็น pure CSS ไม่มี library
ลอง hover ที่ปุ่ม — สังเกตว่ามันค่อยๆ เปลี่ยน ไม่กระตุก
กดปุ่มเพื่อดู animation แต่ละแบบ
แต่ละ card เข้ามาทีละตัว — ใช้ animation-delay + nth-child
ลอง hover แต่ละ card — ทุกตัวใช้แค่ transform + transition
สังเกตว่าแต่ละ timing function เคลื่อนที่ต่างกัน — ease-out (เริ่มเร็ว จบช้า) ดูเป็นธรรมชาติที่สุด
CSS Transition — animation แบบง่ายที่สุด
Section titled “CSS Transition — animation แบบง่ายที่สุด”transition ทำให้การเปลี่ยนแปลง CSS property เกิดขึ้นแบบ ค่อยๆ เปลี่ยน แทนที่จะเปลี่ยนทันที
CSS Transition vs No Transition
ลอง hover ทั้งสองปุ่ม — สังเกตความต่างของ "กระตุก" กับ "ค่อยๆ เปลี่ยน"
.button { background-color: #3b82f6; color: white; padding: 12px 24px; border-radius: 8px; border: none; cursor: pointer;
/* ทำให้ทุก property ที่เปลี่ยน ค่อยๆ เปลี่ยนใน 0.2 วินาที */ transition: all 0.2s ease;}
.button:hover { background-color: #2563eb; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);}transition syntax
Section titled “transition syntax”transition: property duration timing-function delay;| ส่วน | ความหมาย | ตัวอย่าง |
|---|---|---|
property | property ไหนที่จะ animate | all, opacity, transform, background-color |
duration | นานแค่ไหน | 0.2s, 0.5s, 300ms |
timing-function | ลักษณะการเคลื่อน | ease, ease-in, ease-out, ease-in-out, linear |
delay | รอก่อนเริ่ม | 0s, 0.1s |
ตัวอย่างจริง: Card hover effect
Section titled “ตัวอย่างจริง: Card hover effect”.card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;}
.card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);}CSS @keyframes — animation แบบกำหนดเอง
Section titled “CSS @keyframes — animation แบบกำหนดเอง”@keyframes ให้คุณกำหนด animation ที่ซับซ้อนกว่า transition — มีหลาย step, เล่นวนซ้ำ, หรือเล่นอัตโนมัติตอนโหลด
กดปุ่มเพื่อดู @keyframes fadeInUp ทำงาน
/* 1. กำหนด keyframes */@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); }}
/* 2. ใช้กับ element */.hero-title { animation: fadeInUp 0.6s ease-out;}animation syntax
Section titled “animation syntax”animation: name duration timing-function delay iteration-count direction fill-mode;/* ตัวอย่างครบ syntax */.element { animation: fadeInUp 0.6s ease-out 0s 1 normal forwards;}
/* แยกเขียนก็ได้ */.element { animation-name: fadeInUp; animation-duration: 0.6s; animation-timing-function: ease-out; animation-delay: 0s; animation-iteration-count: 1; /* infinite = เล่นซ้ำ */ animation-fill-mode: forwards; /* ค้างที่ท่าสุดท้าย */}| Property | ค่าที่ใช้บ่อย | หมายเหตุ |
|---|---|---|
iteration-count | 1, 3, infinite | จำนวนรอบ |
direction | normal, reverse, alternate | ทิศทาง |
fill-mode | forwards, backwards, both | ค้างที่ท่าไหนหลังจบ |
ตัวอย่าง: Stagger animation (ทำเองด้วย CSS)
Section titled “ตัวอย่าง: Stagger animation (ทำเองด้วย CSS)”@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); }}
.stagger-item { opacity: 0; animation: fadeInUp 0.5s ease-out forwards;}
/* ใช้ delay เพิ่มทีละ 0.1s */.stagger-item:nth-child(1) { animation-delay: 0.0s; }.stagger-item:nth-child(2) { animation-delay: 0.1s; }.stagger-item:nth-child(3) { animation-delay: 0.2s; }.stagger-item:nth-child(4) { animation-delay: 0.3s; }<div class="card-grid"> <div class="stagger-item">Card 1</div> <div class="stagger-item">Card 2</div> <div class="stagger-item">Card 3</div> <div class="stagger-item">Card 4</div></div>CSS animation สำหรับ scroll (Scroll-triggered)
Section titled “CSS animation สำหรับ scroll (Scroll-triggered)”CSS สมัยใหม่มี @scroll-timeline แต่ยังไม่รองรับทุก browser — วิธีที่ใช้ได้จริงตอนนี้คือ IntersectionObserver + CSS class
// เมื่อ element เข้ามาในจอ → เพิ่ม class "visible"const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add("visible"); } }); }, { threshold: 0.1 });
document.querySelectorAll(".animate-on-scroll").forEach((el) => { observer.observe(el);});.animate-on-scroll { opacity: 0; transform: translateY(30px); transition: opacity 0.6s ease, transform 0.6s ease;}
.animate-on-scroll.visible { opacity: 1; transform: translateY(0);}ข้อจำกัดของ CSS Animation
Section titled “ข้อจำกัดของ CSS Animation”| ทำได้ | ทำไม่ได้ / ทำยาก |
|---|---|
| Hover, focus effects | Exit animation (element หายไป) |
| Page load animation | Stagger อัตโนมัติ |
| Scroll-triggered (ต้องใช้ JS ช่วย) | Drag & gesture |
| Infinite loops (loading spinner) | Layout animation (ย้ายตำแหน่ง) |
| Simple state transitions | Orchestration (จัดลำดับ animation ซับซ้อน) |
prefers-reduced-motion — เคารพผู้ใช้
Section titled “prefers-reduced-motion — เคารพผู้ใช้”บาง user ปิด animation ในระบบ (เช่น คนเป็น motion sickness) — เราต้องเคารพ setting นี้
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }}Part 2: Framer Motion — Animation สำหรับ React
Section titled “Part 2: Framer Motion — Animation สำหรับ React”เมื่อเข้าใจ CSS animation แล้ว มาดูว่า Framer Motion ทำให้ทุกอย่าง ง่ายขึ้น อย่างไร
ติดตั้ง
Section titled “ติดตั้ง”npm install motionCSS vs Framer Motion — เปรียบเทียบ
Section titled “CSS vs Framer Motion — เปรียบเทียบ”/* ต้องเขียน keyframes + class + JS observer */@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); }}.element { opacity: 0; animation: fadeInUp 0.6s ease-out forwards;}.stagger:nth-child(1) { animation-delay: 0.0s; }.stagger:nth-child(2) { animation-delay: 0.1s; }.stagger:nth-child(3) { animation-delay: 0.2s; }// แค่ใส่ props — จบ<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease: "easeOut" }}> Content</motion.div>| Feature | CSS | Framer Motion |
|---|---|---|
| Hover effect | transition + :hover | whileHover={{ }} |
| Page load animation | @keyframes + animation | initial + animate |
| Stagger | nth-child + delay ทีละตัว | staggerChildren: 0.1 |
| Exit animation | ทำไม่ได้ | AnimatePresence + exit |
| Scroll animation | JS + IntersectionObserver | whileInView={{ }} |
| Drag | ทำไม่ได้ | drag prop |
พื้นฐาน: motion component
Section titled “พื้นฐาน: motion component”แค่เปลี่ยน <div> เป็น <motion.div> ก็ animate ได้แล้ว
import { motion } from "motion/react";
function FadeInBox() { return ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5 }} className="p-8 bg-blue-500 rounded-xl text-white" > สวัสดี! ฉันค่อยๆ ปรากฏ </motion.div> );}3 Props หลัก
Section titled “3 Props หลัก”| Prop | ความหมาย | ตัวอย่าง |
|---|---|---|
initial | สถานะเริ่มต้น | { opacity: 0, y: 20 } |
animate | สถานะปลายทาง | { opacity: 1, y: 0 } |
transition | ความเร็ว + เอฟเฟกต์ | { duration: 0.5, ease: "easeOut" } |
// ตัวอย่าง: เลื่อนขึ้นมาพร้อม fade in<motion.div initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease: "easeOut" }}> <h1>Welcome</h1></motion.div>Hover & Tap Animation
Section titled “Hover & Tap Animation”translateY(-6px) scale(1.06) box-shadow + border ลอง hover แต่ละ card — ทุกตัวใช้ transition + transform/box-shadow
function AnimatedButton() { return ( <motion.button whileHover={{ scale: 1.05, backgroundColor: "#2563eb" }} whileTap={{ scale: 0.95 }} transition={{ type: "spring", stiffness: 300 }} className="px-6 py-3 bg-blue-600 text-white rounded-xl font-medium" > Click Me </motion.button> );}Card Hover Effect
Section titled “Card Hover Effect”function ProjectCard({ title, description }) { return ( <motion.div whileHover={{ y: -8, boxShadow: "0 20px 40px rgba(0,0,0,0.15)" }} transition={{ type: "spring", stiffness: 200 }} className="p-6 bg-white rounded-2xl border border-gray-200" > <h3 className="text-lg font-bold mb-2">{title}</h3> <p className="text-gray-600">{description}</p> </motion.div> );}Variants — จัดการ Animation หลายตัว
Section titled “Variants — จัดการ Animation หลายตัว”Variants ให้ตั้งชื่อ state ได้ — ใช้สั่ง children animate พร้อมกัน
const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1, // children แต่ละตัวเข้ามาห่างกัน 0.1s }, },};
const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0 },};
function SkillList({ skills }) { return ( <motion.ul variants={containerVariants} initial="hidden" animate="visible" className="space-y-3" > {skills.map((skill) => ( <motion.li key={skill} variants={itemVariants} className="p-4 bg-gray-50 rounded-lg" > {skill} </motion.li> ))} </motion.ul> );}Scroll Animation
Section titled “Scroll Animation”ใช้ whileInView ทำให้ element animate เมื่อ scroll มาถึง
Scroll down inside this container
จำลอง whileInView — element จะ animate เมื่อ scroll มาถึง
function Section({ title, children }) { return ( <motion.section initial={{ opacity: 0, y: 50 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-100px" }} transition={{ duration: 0.6, ease: "easeOut" }} className="py-20" > <h2 className="text-3xl font-bold mb-8">{title}</h2> {children} </motion.section> );}
// ใช้งานfunction Portfolio() { return ( <main> <Section title="About Me"> <p>...</p> </Section> <Section title="My Skills"> <SkillList skills={["React", "Tailwind", "Figma"]} /> </Section> <Section title="Projects"> <p>...</p> </Section> </main> );}Scroll Progress Bar
Section titled “Scroll Progress Bar”import { motion, useScroll } from "motion/react";
function ScrollProgress() { const { scrollYProgress } = useScroll();
return ( <motion.div style={{ scaleX: scrollYProgress }} className="fixed top-0 left-0 right-0 h-1 bg-blue-600 origin-left z-50" /> );}Page Transition (AnimatePresence)
Section titled “Page Transition (AnimatePresence)”AnimatePresence ทำให้ element มี exit animation ก่อนหายไป
import { AnimatePresence, motion } from "motion/react";
function Notification({ message, isVisible }) { return ( <AnimatePresence> {isVisible && ( <motion.div initial={{ opacity: 0, y: -20, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -20, scale: 0.95 }} transition={{ duration: 0.3 }} className="fixed top-4 right-4 px-6 py-3 bg-green-500 text-white rounded-xl shadow-lg" > {message} </motion.div> )} </AnimatePresence> );}Animate List Items
Section titled “Animate List Items”function AnimatedTodoList({ todos, onDelete }) { return ( <AnimatePresence> {todos.map((todo) => ( <motion.li key={todo.id} initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0, marginBottom: 0 }} transition={{ duration: 0.25 }} className="overflow-hidden" > <div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg mb-2"> <span>{todo.text}</span> <button onClick={() => onDelete(todo.id)}>Delete</button> </div> </motion.li> ))} </AnimatePresence> );}Transition Types
Section titled “Transition Types”| Type | ลักษณะ | เหมาะกับ |
|---|---|---|
tween | เคลื่อนที่เรียบ ตั้งเวลาได้ | fade, slide |
spring | เด้งเหมือนสปริง | ปุ่ม, card, interactive |
inertia | หยุดค่อยๆ เหมือนจริง | drag, swipe |
// Spring — รู้สึก natural<motion.div animate={{ x: 100 }} transition={{ type: "spring", stiffness: 200, damping: 20 }}/>
// Tween — ควบคุมได้แม่นยำ<motion.div animate={{ opacity: 1 }} transition={{ type: "tween", duration: 0.5, ease: "easeInOut" }}/>ตัวอย่าง: Hero Section พร้อม Animation
Section titled “ตัวอย่าง: Hero Section พร้อม Animation”function Hero() { return ( <section className="min-h-screen flex items-center justify-center"> <div className="text-center"> <motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0 }} className="text-sm text-blue-600 mb-4" > Welcome to my portfolio </motion.p>
<motion.h1 initial={{ opacity: 0, y: 30 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2, duration: 0.8 }} className="text-5xl font-bold mb-6" > I'm Alex, a<br /> <span className="text-blue-600">UX/UI Engineer</span> </motion.h1>
<motion.p initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.4, duration: 0.6 }} className="text-gray-600 mb-8 max-w-md mx-auto" > I design and build beautiful web experiences. </motion.p>
<motion.button initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.6, type: "spring" }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} className="px-8 py-3 bg-blue-600 text-white rounded-xl font-medium" > View My Work </motion.button> </div> </section> );}Performance Tips
Section titled “Performance Tips”ใช้ transform + opacity เท่านั้นถ้าเป็นไปได้
Section titled “ใช้ transform + opacity เท่านั้นถ้าเป็นไปได้”// Fast — ใช้ GPU ไม่ trigger layout<motion.div animate={{ x: 100, opacity: 1, scale: 1.1 }} />
// Slow — trigger layout recalculation<motion.div animate={{ width: "200px", height: "300px", top: "50px" }} />useReducedMotion — รองรับ accessibility
Section titled “useReducedMotion — รองรับ accessibility”import { useReducedMotion } from "motion/react";
function Card({ children }) { const shouldReduce = useReducedMotion();
return ( <motion.div initial={{ opacity: 0, y: shouldReduce ? 0 : 30 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: shouldReduce ? 0 : 0.6 }} > {children} </motion.div> );}Combining Animations
Section titled “Combining Animations”delay chaining — elements เข้ามาตามลำดับ
Section titled “delay chaining — elements เข้ามาตามลำดับ”function Hero() { return ( <div> <motion.p animate={{ opacity: 1 }} transition={{ delay: 0 }}> subtitle </motion.p> <motion.h1 animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}> Main Title </motion.h1> <motion.button animate={{ opacity: 1 }} transition={{ delay: 0.5 }}> CTA </motion.button> </div> );}layoutId — Shared Layout Animation
Section titled “layoutId — Shared Layout Animation”// เมื่อ element มี layoutId เหมือนกัน Framer Motion จะ animate ระหว่าง 2 จุดอัตโนมัติfunction Tabs({ activeTab, setActiveTab }) { return ( <div className="flex gap-2"> {["Design", "Code", "Deploy"].map((tab) => ( <button key={tab} onClick={() => setActiveTab(tab)} className="relative px-4 py-2"> {tab} {activeTab === tab && ( <motion.div layoutId="active-tab" className="absolute inset-0 bg-blue-500/20 rounded-lg" /> )} </button> ))} </div> );}Real-world Patterns
Section titled “Real-world Patterns”Loading Skeleton
Section titled “Loading Skeleton”function Skeleton() { return ( <motion.div className="h-4 bg-gray-200 rounded" animate={{ opacity: [0.5, 1, 0.5] }} transition={{ duration: 1.5, repeat: Infinity }} /> );}Toast Notification
Section titled “Toast Notification”function Toast({ message, isVisible }) { return ( <AnimatePresence> {isVisible && ( <motion.div initial={{ opacity: 0, y: 50, scale: 0.9 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 20, scale: 0.9 }} className="fixed bottom-6 right-6 px-6 py-3 bg-green-500 text-white rounded-xl shadow-lg" > {message} </motion.div> )} </AnimatePresence> );}Accordion / Collapse
Section titled “Accordion / Collapse”function Accordion({ title, children, isOpen, onClick }) { return ( <div className="border rounded-lg overflow-hidden"> <button onClick={onClick} className="w-full p-4 text-left font-medium"> {title} <motion.span animate={{ rotate: isOpen ? 180 : 0 }} className="float-right"> ▼ </motion.span> </button> <AnimatePresence> {isOpen && ( <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden" > <div className="p-4 border-t">{children}</div> </motion.div> )} </AnimatePresence> </div> );}Best Practices & Style Guide
Section titled “Best Practices & Style Guide”Animation Timing Guidelines
Section titled “Animation Timing Guidelines”| ประเภท | Duration | ตัวอย่าง |
|---|---|---|
| Micro-interaction | 100-200ms | Button hover, toggle |
| Element transition | 200-400ms | Card appear, modal |
| Page transition | 300-600ms | Route change, hero |
| Decorative | 1000ms+ | Background, parallax |
แยก variants เป็นไฟล์
Section titled “แยก variants เป็นไฟล์”export const fadeInUp = { hidden: { opacity: 0, y: 30 }, visible: { opacity: 1, y: 0 },};
export const staggerContainer = { hidden: {}, visible: { transition: { staggerChildren: 0.1 } },};// ใช้ในหลาย component ได้import { fadeInUp, staggerContainer } from "../animations/variants";Do / Don’t
Section titled “Do / Don’t”// Don't: animate ทุกอย่าง — ทำให้เว็บ "วุ่นวาย"<motion.div whileHover={{ scale: 1.2, rotate: 10, y: -20 }}>...</motion.div>
// Do: subtle animation — สังเกตแทบไม่ออกแต่รู้สึกดี<motion.div whileHover={{ scale: 1.02, y: -2 }}>...</motion.div>// Don't: duration ยาวเกินไป ผู้ใช้ต้องรอ<motion.div transition={{ duration: 2 }}>...</motion.div>
// Do: เร็วพอที่จะไม่รู้สึกว่ารอ<motion.div transition={{ duration: 0.3 }}>...</motion.div>Accessibility
Section titled “Accessibility”// รองรับ prefers-reduced-motion เสมอconst shouldReduce = useReducedMotion();
<motion.div initial={shouldReduce ? false : { opacity: 0 }} animate={{ opacity: 1 }}/>ลองทำเอง
Section titled “ลองทำเอง”- Fade + Slide In — สร้าง card ที่ fade in + เลื่อนขึ้นมาตอนโหลดหน้า
- Hover Effect — สร้างปุ่มที่ขยายตอน hover, หดตอน tap
- Stagger List — สร้าง skill list ที่ items เข้ามาทีละตัว
- Scroll Animation — ทำแต่ละ section ให้ animate เมื่อ scroll ถึง
- AnimatePresence — ทำ notification ที่ slide in/out ได้