跳至主要内容
Simple React Scroll Animations With Zero Dependencies

from Bret Cameron

Beginner

/

React / Next
HTML / CSS

我們可以透過 IntersectionObserver 來偵測元素是否在視窗中,並且透過 CSS transition 來實現滾動時的動畫效果。這個方法不需要任何第三方套件,而且可以製作成一個通用的動畫元件,讓開發者可以自行決定動畫的效果。

Scroll Demo
Scroll Demo

Step 1. Create a custom hook useElementOnScreen

首先,我們先建立一個自訂的 hook useElementOnScreen 來偵測元素是否在視窗中。裡面的 rootMargin 參數可以讓我們設定一個偏移量,讓動畫在視窗邊緣的某個距離開始,而 0px 則是正好在邊緣。

function useElementOnScreen(ref: RefObject<Element>, rootMargin = "0px") {
const [isIntersecting, setIsIntersecting] = useState(true);

useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsIntersecting(entry.isIntersecting);
},
{ rootMargin }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
if (ref.current) {
observer.unobserve(ref.current);
}
};
}, []);
return isIntersecting;
}

Step 2. Wrap the hook and animation into a container component

第二步,我們將 useElementOnScreen hook 和 CSS transition 整合到一個 container component 中。 useElementOnScreen hook 會回傳一個 boolean 值,我們可以利用這個值來決定元素的 opacitytranslate 效果。 opacity 會在元素進入視窗時變為 1,而 translate 則是在元素進入視窗時會從 0 2rem 變為 none,也就是沒有位移。

const AnimateIn: FC<PropsWithChildren> = ({ children }) => {
const ref = useRef<HTMLDivElement>(null);
const onScreen = useElementOnScreen(ref);

return (
<div
ref={ref}
style={{
opacity: onScreen ? 1 : 0,
translate: onScreen ? "none" : "0 2rem",
transition: "600ms ease-in-out",
}}
>
{children}
</div>
);
};

要使用這個 container,我們只需要將我們的元件包在 children 即可。到這邊,我們已經建立了一個可以做淡入動畫的元件,而且不需要任何第三方套件。

<AnimateIn>
<h1>Hello World</h1>
</AnimateIn>

Bonus: Generic Animated Container

我們可以進一步將 <AnimatedIn> 元件變得更通用,透過傳入 CSSProperties 來決定元件的 fromto 狀態,讓開發者可以自行決定動畫的效果。

const AnimateIn: FC<
PropsWithChildren<{ from: CSSProperties; to: CSSProperties }>
> = ({ from, to, children }) => {
const ref = useRef<HTMLDivElement>(null);
const onScreen = useElementOnScreen(ref);
const defaultStyles: CSSProperties = {
transition: "600ms ease-in-out",
};
return (
<div
ref={ref}
style={
onScreen
? {
...defaultStyles,
...to,
}
: {
...defaultStyles,
...from,
}
}
>
{children}
</div>
);
};

現在我們可以傳入不同的 fromto CSSProperties 來產生不同的動畫元件。

const FadeIn: FC<PropsWithChildren> = ({ children }) => (
<AnimateIn from={{ opacity: 0 }} to={{ opacity: 1 }}>
{children}
</AnimateIn>
);

const FadeUp: FC<PropsWithChildren> = ({ children }) => (
<AnimateIn
from={{ opacity: 0, translate: "0 2rem" }}
to={{ opacity: 1, translate: "none" }}
>
{children}
</AnimateIn>
);

const ScaleIn: FC<PropsWithChildren> = ({ children }) => (
<AnimateIn from={{ scale: "0" }} to={{ scale: "1" }}>
{children}
</AnimateIn>
);

export const Animate = {
FadeIn,
FadeUp,
ScaleIn,
};

<Animate.ScaleIn>
<h1>Hello World</h1>
</Animate.ScaleIn>;