Sometimes, all you need is a subtle fade or slide animation when elements enter the viewport. You donβt always need a heavy library like framer-motion
for that.
In this blog, I'll walk through a lightweight Reveal
component which I built with React and the Intersection Observer API. It mimics the basic motion.div
behavior but only for simple fade-in animations (up, down, left, right).
Component code (Reveal.tsx)
"use client";
import { ElementType, HTMLAttributes } from "react";
import cn from "classnames";
import { useInView } from "@/hooks/useInView";
type RevealProps = {
as?: ElementType;
className?: string;
delay?: number;
position?: "top" | "bottom" | "left" | "right";
} & HTMLAttributes<HTMLElement>;
export const Reveal = ({
as,
className,
delay = 0,
position = "bottom",
children,
style,
...rest
}: RevealProps) => {
const { ref, inView } = useInView<HTMLElement>();
const Tag: ElementType = as ?? "div";
const getPositionClass = () => {
switch (position) {
case "top":
return "reveal-slide-top";
case "left":
return "reveal-slide-left";
case "right":
return "reveal-slide-right";
case "bottom":
default:
return "reveal-slide-bottom";
}
};
return (
<Tag
ref={ref as any}
{...rest}
className={cn(
getPositionClass(),
"opacity-0 will-change-transform",
inView && "reveal-in",
className
)}
style={{ ...style, transitionDelay: `${delay}ms` }}
>
{children}
</Tag>
);
};
Custom hook code (useInView.ts
)
"use client";
import { useEffect, useRef, useState } from "react";
export const useInView = <T extends Element>(
options?: IntersectionObserverInit
) => {
const ref = useRef<T | null>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === "undefined") return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
obs.unobserve(entry.target); // one-shot for performance
}
},
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1, ...options }
);
obs.observe(el);
return () => obs.disconnect();
}, [options]);
return { ref, inView };
};
CSS code
.reveal {
transition: all 0.4s ease-in-out;
transform:translateY(16px);
opacity:0;
}
.reveal-in {
transform:translateY(0);
opacity:1;
}
.reveal-slide-left {
transition: all 0.4s ease-out;
transform:translateX(-30px);
opacity:0;
}
.reveal-slide-left.reveal-in {
transform:translateX(0);
opacity:1;
}
.reveal-slide-right {
transition: all 0.4s ease-out;
transform:translateX(30px);
opacity:0;
}
.reveal-slide-right.reveal-in {
transform:translateX(0);
opacity:1;
}
.reveal-slide-top {
transition: all 0.4s ease-out;
transform:translateY(-30px);
opacity:0;
}
.reveal-slide-top.reveal-in {
transform:translateY(0);
opacity:1;
}
.reveal-slide-bottom {
transition: all 0.4s ease-out;
transform:translateY(30px);
opacity:0;
}
.reveal-slide-bottom.reveal-in {
transform:translateY(0);
opacity:1;
}
And just like that, you have a re-usable component which you can use to add fade animation to almost any element you want!
Simple usage
<Reveal>
<p>Animate me!</p>
</Reveal>
By default it it will fade in from the bottom because of the component's position
is set to bottom.
Advanced Usage (directional, delay)
<Reveal position="top">
<p>Animate me from top</p>
</Reveal>
<Reveal position="left">
<p>Animate me from left</p>
</Reveal>
<Reveal position="right">
<p>Animate me from right</p>
</Reveal>
<Reveal position="left" delay={200}>
<p>Animate me from left with delay</p>
</Reveal>
<Reveal position="right" delay={400}>
<p>Animate me from right with delay</p>
</Reveal>
Look at how I used the delay prop to add a delay in the animation, the delay value is judge in milliseconds so 1000 = 1 second.
The custom useInView
takes the ref of the component, observes its position in the viewport, then return the inView
value, if the element is in view, the value returns true which and vice-versa. The inView
state variable is responsible for adding animation class reveal-in
Conclusion: A highly-robust, light-weight and easy-to-use animation component that elements the need of any third-party library to add simple animations. Feel free to add more to this component, Ciao!