Bài 26.3: Framer Motion trong TypeScript

Bài 26.3: Framer Motion trong TypeScript
Chào mừng bạn đến với bài viết chuyên sâu về việc mang sự sống động vào ứng dụng React của bạn bằng Framer Motion, kết hợp với sự mạnh mẽ và an toàn của TypeScript. Animation không chỉ làm cho giao diện người dùng trở nên đẹp mắt hơn mà còn giúp cải thiện trải nghiệm người dùng bằng cách cung cấp phản hồi trực quan và dẫn dắt sự chú ý của họ. Framer Motion là một thư viện animation cực kỳ phổ biến và mạnh mẽ cho React, làm cho việc tạo ra các hiệu ứng động phức tạp trở nên đơn giản đáng ngạc nhiên.
Tại sao lại là Framer Motion?
Có rất nhiều thư viện animation cho React, vậy tại sao Framer Motion lại nổi bật?
- Dễ sử dụng: Cú pháp trực quan, tập trung vào các component React. Bạn chỉ cần thêm các props vào component của mình.
- Mạnh mẽ: Hỗ trợ nhiều loại animation (layout, gestures, scroll, v.v.), vật lý (spring physics), và các tính năng nâng cao như
variants
để quản lý state animation. - Hiệu suất cao: Được tối ưu hóa để chạy mượt mà, ngay cả trên các thiết bị di động.
- Hỗ trợ TypeScript tuyệt vời: Đây là điểm cộng lớn mà chúng ta sẽ đi sâu vào.
Sức mạnh của TypeScript khi kết hợp với Framer Motion
Khi làm việc với animation, đặc biệt là với một thư viện nhiều tính năng như Framer Motion, bạn sẽ phải xử lý rất nhiều thuộc tính style và cấu hình animation. TypeScript mang lại những lợi ích to lớn:
- An toàn kiểu dữ liệu: TypeScript giúp bạn bắt lỗi ngay tại thời điểm biên dịch thay vì lúc chạy. Bạn sẽ biết ngay nếu nhập sai tên thuộc tính animation hoặc sử dụng sai kiểu giá trị.
- Tự động hoàn thành (Autocompletion): Trình chỉnh sửa code của bạn (VS Code, WebStorm,...) sẽ cung cấp gợi ý đầy đủ các thuộc tính mà bạn có thể sử dụng với
motion
component, giúp tăng tốc độ code và giảm lỗi chính tả. - Hiểu rõ cấu trúc: Khi làm việc với
variants
hoặc các props phức tạp khác, TypeScript giúp bạn hiểu rõ cấu trúc dữ liệu cần thiết.
Framer Motion được viết bằng TypeScript và cung cấp định nghĩa kiểu dữ liệu hoàn chỉnh, làm cho trải nghiệm phát triển với TypeScript trở nên mượt mà và tin cậy.
Bắt đầu với Framer Motion và TypeScript
Đầu tiên, bạn cần cài đặt thư viện:
npm install framer-motion
# hoặc
yarn add framer-motion
Sau đó, bạn có thể bắt đầu sử dụng. Cú pháp cơ bản là thêm tiền tố motion.
vào bất kỳ component HTML hoặc React nào bạn muốn tạo animation.
import { motion } from "framer-motion";
function MyComponent() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }} // Trạng thái ban đầu
animate={{ opacity: 1, y: 0 }} // Trạng thái đích (khi component mount)
transition={{ duration: 0.5 }} // Cấu hình chuyển động
className="box"
>
Xin chào, Framer Motion!
</motion.div>
);
}
Giải thích:
- Chúng ta import
motion
từ thư việnframer-motion
. motion.div
tạo ra một thẻdiv
có khả năng animation. Tương tự, bạn cómotion.span
,motion.button
,motion.img
,motion.svg
,motion.path
, hoặc thậm chí làmotion(CustomReactComponent)
.initial
: Định nghĩa trạng thái bắt đầu của animation. Ở đây làopacity: 0
(trong suốt) vày: 20
(dịch xuống 20px). TypeScript giúp bạn gợi ý các thuộc tính CSS hoặc transform hợp lệ.animate
: Định nghĩa trạng thái kết thúc của animation. Khi component được render, nó sẽ tự động chuyển từ trạng tháiinitial
đếnanimate
. Ở đây làopacity: 1
(hiện rõ) vày: 0
(trở về vị trí ban đầu).transition
: Cấu hình cách thức animation diễn ra.duration: 0.5
nghĩa là animation này sẽ kéo dài 0.5 giây. TypeScript sẽ gợi ý các tùy chọn khác nhưtype
('spring', 'tween', 'inertia'),delay
,ease
, v.v.
Với TypeScript, khi bạn gõ initial={{ }}
hoặc animate={{ }}
, bạn sẽ nhận được gợi ý tuyệt vời về các thuộc tính style và transform có thể animate, cùng với kiểu dữ liệu mong đợi cho từng thuộc tính. Điều này giúp bạn tránh gõ sai 'opacit'
thay vì 'opacity'
hoặc gán một string vào thuộc tính cần number.
Ví dụ: Hiệu ứng di chuột (Hover)
Framer Motion làm cho các hiệu ứng tương tác trở nên cực kỳ đơn giản với các props như whileHover
, whileTap
, whileFocus
, v.v.
import { motion } from "framer-motion";
import React from "react"; // Import React nếu cần JSX
function HoverScaleButton() {
return (
<motion.button
whileHover={{ scale: 1.1, backgroundColor: "#3498db" }} // Scale up and change color on hover
whileTap={{ scale: 0.9 }} // Scale down slightly on tap/click
transition={{ type: "spring", stiffness: 400, damping: 10 }} // Use spring physics
className="my-button"
style={{
padding: '10px 20px',
fontSize: '16px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
backgroundColor: '#2ecc71',
color: 'white'
}}
>
Hover Me!
</motion.button>
);
}
Giải thích:
motion.button
là một thẻ<button>
có khả năng animation.whileHover
: Định nghĩa animation sẽ chạy khi con trỏ chuột di qua component. Ở đây, nó sẽ phóng toscale: 1.1
và đổi màu nềnbackgroundColor: "#3498db"
.whileTap
: Định nghĩa animation sẽ chạy khi component bị nhấn (click hoặc tap trên di động). Ở đây, nó thu nhỏ lại một chútscale: 0.9
.transition
: Chúng ta sử dụngtype: "spring"
để tạo hiệu ứng nhún (spring physics), làm cho animation trở nên tự nhiên và mượt mà hơn.stiffness
vàdamping
là các tham số điều chỉnh hiệu ứng spring.- TypeScript đảm bảo rằng các thuộc tính bên trong
whileHover
,whileTap
, vàtransition
đều hợp lệ và đúng kiểu.
Ví dụ: Sử dụng Variants cho Animation phức tạp hơn
variants
là một tính năng mạnh mẽ trong Framer Motion giúp bạn quản lý các trạng thái animation một cách rõ ràng và dễ dàng tạo animation cho các danh sách (list orchestration).
import { motion, Variants } from "framer-motion";
import React from "react";
// Định nghĩa variants bằng TypeScript Interface hoặc type
// Điều này giúp đảm bảo cấu trúc của variants là chính xác
interface ContainerVariants extends Variants {
hidden: {};
visible: {
transition: {
staggerChildren: number; // staggerChildren là thuộc tính của transition
};
};
}
interface ItemVariants extends Variants {
hidden: { opacity: number; y: number };
visible: { opacity: number; y: number };
}
const containerVariants: ContainerVariants = {
hidden: { opacity: 1 },
visible: {
opacity: 1,
transition: {
delayChildren: 0.3, // Độ trễ trước khi animation của các con bắt đầu
staggerChildren: 0.2 // Độ trễ giữa các item con
}
}
};
const itemVariants: ItemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
function ListItemAnimation() {
return (
<motion.ul
variants={containerVariants} // Áp dụng variants cho container
initial="hidden" // Trạng thái ban đầu của container
animate="visible" // Trạng thái đích của container
style={{ listStyle: 'none', padding: 0 }}
>
{["Item 1", "Item 2", "Item 3", "Item 4"].map((item, index) => (
<motion.li
key={index}
variants={itemVariants} // Áp dụng variants cho từng item
style={{
backgroundColor: '#ecf0f1',
margin: '10px 0',
padding: '10px',
borderRadius: '5px'
}}
>
{item}
</motion.li>
))}
</motion.ul>
);
}
Giải thích:
- Chúng ta định nghĩa hai object
variants
:containerVariants
cho thẻ<ul>
vàitemVariants
cho từng thẻ<li>
. - Mỗi object
variants
chứa các key biểu thị "trạng thái" của animation (ví dụ:"hidden"
,"visible"
). Giá trị của mỗi key là một object style/transform hoặc một object transition. - TypeScript: Chúng ta sử dụng Interface (
ContainerVariants
,ItemVariants
) để định nghĩa cấu trúc mong đợi của các object variants. Điều này rất hữu ích vì cấu trúc củavariants
có thể phức tạp (ví dụ:transition
nằm lồng bên trong trạng thái), và TypeScript giúp bạn đảm bảo bạn đang định nghĩa đúng. - Trên thẻ
motion.ul
(container), chúng ta gánvariants={containerVariants}
,initial="hidden"
, vàanimate="visible"
. Khi container chuyển từ trạng thái"hidden"
sang"visible"
, nó sẽ kích hoạt animation của chính nó (trong trường hợp này là opacity của container không đổi, nhưng transition chứastaggerChildren
). - Trên mỗi thẻ
motion.li
(item), chúng ta gánvariants={itemVariants}
. Bởi vì chúng nằm trong mộtmotion
component cha sử dụngvariants
và cóstaggerChildren
trong transition, Framer Motion sẽ tự động điều phối animation của từng item con dựa trênitemVariants
. staggerChildren
: Thuộc tính trongtransition
của container variants. Nó tạo ra một độ trễ giữa thời điểm bắt đầu animation của các item con, tạo ra hiệu ứng "lượn sóng" đẹp mắt.
Ví dụ này cho thấy sức mạnh của variants
trong việc quản lý state animation và cách Framer Motion xử lý việc phối hợp animation cho các danh sách một cách tự động. Và TypeScript giúp bạn định nghĩa các variants này một cách an toàn và có cấu trúc.
Sử dụng Hooks để điều khiển Animation
Framer Motion cung cấp các hooks mạnh mẽ để kiểm soát animation một cách thủ công hoặc phản ứng với các sự kiện phức tạp hơn. Một hook phổ biến là useAnimationControls
(hoặc useAnimate
trong các phiên bản mới hơn).
import { motion, useAnimationControls } from "framer-motion";
import React from "react";
function ManualAnimation() {
const controls = useAnimationControls(); // Khởi tạo controls
const startAnimation = async () => {
await controls.start({ x: 100, rotate: 360 }); // Chạy animation 1
await controls.start({ x: 0, rotate: 0 }); // Chạy animation 2
};
return (
<div>
<motion.div
animate={controls} // Gắn controls vào component
style={{
width: 50,
height: 50,
backgroundColor: '#e74c3c',
borderRadius: '10px',
margin: '20px 0'
}}
/>
<button onClick={startAnimation}>
Start Animation
</button>
</div>
);
}
Giải thích:
useAnimationControls()
: Hook này trả về một objectcontrols
mà bạn có thể sử dụng để bắt đầu hoặc dừng animation.- Chúng ta gắn object
controls
này vào componentmotion.div
thông qua propanimate={controls}
. Điều này báo cho Framer Motion biết rằng animation của component này sẽ được điều khiển bởi objectcontrols
. - Hàm
startAnimation
là một hàmasync
sử dụngcontrols.start()
.controls.start()
có thể nhận một object style/transform (giống như propanimate
) hoặc tên của một variant. Nó trả về một Promise, cho phép chúng ta đợi animation hiện tại kết thúc trước khi bắt đầu animation tiếp theo (ví dụ: chạy đến x=100, sau đó mới quay về x=0). - Nút bấm gọi hàm
startAnimation
khi được click, kích hoạt chuỗi animation. - TypeScript giúp đảm bảo các thuộc tính bạn truyền vào
controls.start()
là hợp lệ.
Các hook khác như useScroll
giúp tạo animation dựa trên vị trí cuộn trang, useTransform
giúp liên kết giá trị của một animation (ví dụ: vị trí cuộn) với giá trị của một thuộc tính khác (ví dụ: opacity hoặc scale).
Tips và Best Practices
- Sử dụng
AnimatePresence
: Khi component của bạn được mount/unmount (thêm/bớt khỏi DOM), hãy bọc chúng trongAnimatePresence
để tạo hiệu ứng fade-in/fade-out hoặc các hiệu ứng vào/ra tùy chỉnh. - Hiệu suất: Đối với các animation phức tạp hoặc trên nhiều phần tử, hãy chú ý đến hiệu suất. Sử dụng các thuộc tính transform (
x
,y
,scale
,rotate
) thường hiệu quả hơn việc animate các thuộc tính layout nhưwidth
,height
,margin
,padding
. - Framer Motion DevTools: Cài đặt extension Framer Motion DevTools cho trình duyệt để kiểm tra và debug animation dễ dàng hơn.
- Kết hợp với CSS/Styled-components: Bạn hoàn toàn có thể sử dụng Framer Motion kết hợp với CSS Modules, Styled Components, hoặc Tailwind CSS. Framer Motion chỉ điều khiển các thuộc tính style động mà bạn chỉ định.
Framer Motion là một công cụ tuyệt vời để thêm sự sống động và tương tác vào ứng dụng React của bạn. Khi kết hợp với TypeScript, bạn không chỉ tạo ra các animation mượt mà mà còn phát triển code một cách an toàn và hiệu quả hơn. Hãy khám phá các tài liệu của Framer Motion để tìm hiểu sâu hơn về tất cả các tính năng mạnh mẽ mà nó cung cấp!
Comments