Komponen AnimatedSection yang bisa dipakai ulang untuk menganimasikan konten saat masuk dan keluar viewport, mengikuti arah dan posisi scroll, dibangun dengan Framer Motion dan React hooks.

Animasi bisa menghidupkan aplikasi web, memperkaya pengalaman pengguna dan membuat antarmuka terasa lebih menarik. Di artikel ini kita akan membahas sebuah komponen React bernama AnimatedSection yang memanfaatkan Framer Motion untuk membuat animasi dinamis berbasis scroll. Komponen ini sepenuhnya bisa disesuaikan, jadi cocok untuk ditambahkan ke proyek kamu.
Komponen AnimatedSection menganimasikan isinya saat masuk atau keluar dari viewport, mengikuti arah dan posisi scroll pengguna. Dengan bantuan Framer Motion dan React hooks, animasinya menyesuaikan diri secara dinamis untuk menghasilkan transisi yang halus.
Beberapa fiturnya:
Mari kita pecah kodenya menjadi bagian-bagian kecil dan jelaskan tiap bagian supaya lebih mudah dipahami.
import React, { useEffect, useState, useRef, PropsWithChildren } from "react";
import { motion, useAnimation, Variants } from "framer-motion";
interface Props {
className?: string;
threshold?: number;
}Di sini kita mengimpor hooks yang diperlukan dari React (useEffect, useState, useRef) dan dari Framer Motion (motion, useAnimation, Variants). Kita mendefinisikan interface Props agar className dan threshold bersifat opsional.
const AnimatedSection: React.FC<PropsWithChildren<Props>> = (props) => {
const { children, className, threshold = 0.25 } = props;
const controls = useAnimation();
const sectionRef = useRef<HTMLDivElement | null>(null);
const [direction, setDirection] = useState<"up" | "down" | null>(null);
const [position, setPosition] = useState<"top" | "bottom" | null>(null);
const [isInView, setInView] = useState<boolean>(false);Di sini kita mendefinisikan kerangka komponennya dan menyiapkan beberapa variabel state:
const handleScroll = () => {
const section = sectionRef.current;
if (!section) return;
const rect = section.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const sectionHeight = section.clientHeight;
const topThreshold = threshold * viewportHeight;
const bottomThreshold = (1 - threshold) * viewportHeight;
if (rect.y + sectionHeight < topThreshold) {
setPosition("top");
} else if (rect.y > bottomThreshold) {
setPosition("bottom");
} else {
setPosition(null); // Fully in view
}
if (rect.y + sectionHeight > 0 && rect.y < viewportHeight) {
setInView(true);
} else setInView(false);
};Fungsi ini memakai getBoundingClientRect() untuk menghitung posisi section relatif terhadap viewport. Ia memperbarui state position dan isInView berdasarkan apakah section berada:
useEffect(() => {
const section = sectionRef.current;
if (!section) return;
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);useEffect ini memasang fungsi handleScroll ke event scroll, dan memastikan listener-nya dilepas kembali saat komponen di-unmount.
useEffect(() => {
let lastScrollY = window.scrollY;
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (currentScrollY > lastScrollY) {
setDirection("down");
} else if (currentScrollY < lastScrollY) {
setDirection("up");
}
lastScrollY = currentScrollY > 0 ? currentScrollY : 0; // Avoid negative scroll values
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);Effect ini melacak arah scroll pengguna dengan membandingkan posisi scroll saat ini terhadap posisi scroll sebelumnya.
useEffect(() => {
if (direction === "down" && position === "top") {
controls.start("scrollingOutTop");
} else if (direction === "up" && position === "bottom") {
controls.start("scrollingOutBottom");
} else {
if (isInView) controls.start("visible");
}
}, [direction, position, isInView, controls]);Effect ini memicu animasi yang sesuai berdasarkan arah scroll, posisi, dan apakah section sedang isInView.
const variants: Variants = {
scrollingOutTop: { opacity: 0, y: -100, transition: { duration: 0.5 } },
scrollingOutBottom: { opacity: 0, y: 100, transition: { duration: 0.5 } },
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
};Objek variants mendefinisikan tiga keadaan animasi:
return (
<motion.div
ref={sectionRef}
animate={controls}
initial="visible"
variants={variants}
className={className}
>
{children}
</motion.div>
);
};
export default AnimatedSection;Terakhir, komponen ini merender sebuah motion.div dari Framer Motion. Prop animate menerapkan keadaan animasi secara dinamis berdasarkan objek controls.
import React, { useEffect, useState, useRef, PropsWithChildren } from "react";
import { motion, useAnimation, Variants } from "framer-motion";
interface Props {
className?: string;
threshold?: number;
}
const AnimatedSection: React.FC<PropsWithChildren<Props>> = (props) => {
const { children, className, threshold = 0.25 } = props;
const controls = useAnimation();
const sectionRef = useRef<HTMLDivElement | null>(null);
const [direction, setDirection] = useState<"up" | "down" | null>(null);
const [position, setPosition] = useState<"top" | "bottom" | null>(null);
const [isInView, setInView] = useState<boolean>(false);
const handleScroll = () => {
const section = sectionRef.current;
if (!section) return;
const rect = section.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const sectionHeight = section.clientHeight;
const topThreshold = threshold * viewportHeight;
const bottomThreshold = (1 - threshold) * viewportHeight;
if (rect.y + sectionHeight < topThreshold) {
setPosition("top");
} else if (rect.y > bottomThreshold) {
setPosition("bottom");
} else {
setPosition(null); // Fully in view
}
if (rect.y + sectionHeight > 0 && rect.y < viewportHeight) {
setInView(true);
} else setInView(false);
};
useEffect(() => {
const section = sectionRef.current;
if (!section) return;
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
useEffect(() => {
let lastScrollY = window.scrollY;
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (currentScrollY > lastScrollY) {
setDirection("down");
} else if (currentScrollY < lastScrollY) {
setDirection("up");
}
lastScrollY = currentScrollY > 0 ? currentScrollY : 0; // Avoid negative scroll values
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
if (direction === "down" && position === "top") {
controls.start("scrollingOutTop");
} else if (direction === "up" && position === "bottom") {
controls.start("scrollingOutBottom");
} else {
if (isInView) controls.start("visible");
}
}, [direction, position, isInView, controls]);
const variants: Variants = {
scrollingOutTop: { opacity: 0, y: -100, transition: { duration: 0.5 } },
scrollingOutBottom: { opacity: 0, y: 100, transition: { duration: 0.5 } },
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
};
return (
<motion.div
ref={sectionRef}
animate={controls}
initial="visible"
variants={variants}
className={className}
>
{children}
</motion.div>
);
};
export default AnimatedSection;Berikut cara mengintegrasikan komponen AnimatedSection ke dalam aplikasi kamu:
import AnimatedSection from "./AnimatedSection";
const App = () => {
return (
<div>
<AnimatedSection threshold={0.2} className="my-16 p-8 bg-gray-200 rounded-md">
<h1 className="text-xl font-bold">Hello, World!</h1>
<p>Scroll to see the animations in action!</p>
</AnimatedSection>
<div className="h-[200vh] bg-gradient-to-b from-white to-gray-300"></div>
</div>
);
};
export default App;Komponen AnimatedSection adalah alat yang ampuh untuk menambahkan animasi dinamis berbasis scroll ke proyek React kamu. Dengan desainnya yang modular dan mudah disesuaikan, ia jadi pilihan yang bagus untuk memperkaya UI. Mulai pakai sekarang dan buat pengunjungmu terpikat dengan animasi yang halus dan interaktif!