Skip to content

Study Guide: Framer Motion

Animation ที่ดีทำให้ UI รู้สึก มีชีวิต — ช่วยบอก user ว่าอะไรเปลี่ยน, อะไรสำคัญ, อะไรกดได้ ในฐานะ Designer ที่เขียนโค้ดได้ คุณจะสร้าง animation ที่ สวยจริง ไม่ใช่แค่ใช้งานได้

เส้นทาง: จาก CSS → Framer Motion

Section titled “เส้นทาง: จาก CSS → Framer Motion”
CSS Transitionhover, focus CSS Animation@keyframes CSS เชิงลึกtransform, opacity Framer MotionReact library Productionscroll, stagger, exit

Part 1: CSS Animation พื้นฐาน (ไม่ต้องใช้ library)

Section titled “Part 1: CSS Animation พื้นฐาน (ไม่ต้องใช้ library)”

ก่อนใช้ Framer Motion ต้องเข้าใจ CSS animation ก่อน — เพราะ Framer Motion สร้างมาบน concept เดียวกัน

ลองกดปุ่ม, hover, และ replay เพื่อดู CSS animation แต่ละแบบ — ทุกอย่างเป็น pure CSS ไม่มี library

style.css LIVE DEMO

ลอง hover ที่ปุ่ม — สังเกตว่ามันค่อยๆ เปลี่ยน ไม่กระตุก

Without transition
With transition
Hello!

กดปุ่มเพื่อดู animation แต่ละแบบ

Card 1
Card 2
Card 3
Card 4
Card 5
Card 6

แต่ละ card เข้ามาทีละตัว — ใช้ animation-delay + nth-child

Lift translateY(-8px)
Scale scale(1.05)
Glow box-shadow
Border border-color

ลอง hover แต่ละ card — ทุกตัวใช้แค่ transform + transition

linear
ease
ease-in
ease-out
ease-in-out

สังเกตว่าแต่ละ timing function เคลื่อนที่ต่างกัน — ease-out (เริ่มเร็ว จบช้า) ดูเป็นธรรมชาติที่สุด


CSS Transition — animation แบบง่ายที่สุด

Section titled “CSS Transition — animation แบบง่ายที่สุด”

transition ทำให้การเปลี่ยนแปลง CSS property เกิดขึ้นแบบ ค่อยๆ เปลี่ยน แทนที่จะเปลี่ยนทันที

Output

CSS Transition vs No Transition

No transition
With transition

ลอง hover ทั้งสองปุ่ม — สังเกตความต่างของ "กระตุก" กับ "ค่อยๆ เปลี่ยน"

style.css
.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: property duration timing-function delay;
ส่วนความหมายตัวอย่าง
propertyproperty ไหนที่จะ animateall, 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”
style.css
.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, เล่นวนซ้ำ, หรือเล่นอัตโนมัติตอนโหลด

Output
Hello!

กดปุ่มเพื่อดู @keyframes fadeInUp ทำงาน

style.css
/* 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: name duration timing-function delay iteration-count direction fill-mode;
style.css
/* ตัวอย่างครบ 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-count1, 3, infiniteจำนวนรอบ
directionnormal, reverse, alternateทิศทาง
fill-modeforwards, backwards, bothค้างที่ท่าไหนหลังจบ

ตัวอย่าง: Stagger animation (ทำเองด้วย CSS)

Section titled “ตัวอย่าง: Stagger animation (ทำเองด้วย CSS)”
style.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; }
index.html
<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

script.js
// เมื่อ 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);
});
style.css
.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 effectsExit animation (element หายไป)
Page load animationStagger อัตโนมัติ
Scroll-triggered (ต้องใช้ JS ช่วย)Drag & gesture
Infinite loops (loading spinner)Layout animation (ย้ายตำแหน่ง)
Simple state transitionsOrchestration (จัดลำดับ animation ซับซ้อน)

prefers-reduced-motion — เคารพผู้ใช้

Section titled “prefers-reduced-motion — เคารพผู้ใช้”

บาง user ปิด animation ในระบบ (เช่น คนเป็น motion sickness) — เราต้องเคารพ setting นี้

style.css
@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 ทำให้ทุกอย่าง ง่ายขึ้น อย่างไร

Terminal window
npm install motion

CSS 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; }
FeatureCSSFramer Motion
Hover effecttransition + :hoverwhileHover={{ }}
Page load animation@keyframes + animationinitial + animate
Staggernth-child + delay ทีละตัวstaggerChildren: 0.1
Exit animationทำไม่ได้AnimatePresence + exit
Scroll animationJS + IntersectionObserverwhileInView={{ }}
Dragทำไม่ได้drag prop

แค่เปลี่ยน <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>
);
}
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>

Output
Lift translateY(-6px)
Scale scale(1.06)
Glow 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>
);
}
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>
);
}

ใช้ whileInView ทำให้ element animate เมื่อ scroll มาถึง

Output

Scroll down inside this container

scroll down
1 Fade In
2 Slide Up
3 Scale In

จำลอง 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>
);
}
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"
/>
);
}

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>
);
}
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>
);
}

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>
);
}

ใช้ 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>
);
}

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>
);
}
// เมื่อ 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>
);
}

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 }}
/>
);
}
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>
);
}
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>
);
}

ประเภทDurationตัวอย่าง
Micro-interaction100-200msButton hover, toggle
Element transition200-400msCard appear, modal
Page transition300-600msRoute change, hero
Decorative1000ms+Background, parallax

แยก variants เป็นไฟล์

Section titled “แยก variants เป็นไฟล์”
src/animations/variants.js
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";
// 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>
// รองรับ prefers-reduced-motion เสมอ
const shouldReduce = useReducedMotion();
<motion.div
initial={shouldReduce ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
/>

  1. Fade + Slide In — สร้าง card ที่ fade in + เลื่อนขึ้นมาตอนโหลดหน้า
  2. Hover Effect — สร้างปุ่มที่ขยายตอน hover, หดตอน tap
  3. Stagger List — สร้าง skill list ที่ items เข้ามาทีละตัว
  4. Scroll Animation — ทำแต่ละ section ให้ animate เมื่อ scroll ถึง
  5. AnimatePresence — ทำ notification ที่ slide in/out ได้