useStaggeredReveal
React hook to reveal list items progressively with a stagger delay.
Made by lucasDesign
Develop
Deploy
Monitor
Iterate
import useStaggeredReveal from "@/components/targetblank/hooks/use-staggered-reveal";
import { Button } from "@/components/ui/button";
const ITEMS = ["Design", "Develop", "Deploy", "Monitor", "Iterate"];
export default function StaggeredRevealDemo() {
const { isVisible, replay } = useStaggeredReveal({
count: ITEMS.length,
stagger: 120,
initialDelay: 100,
});
return (
<div className="flex flex-col gap-6 w-[360px]">
<div className="flex flex-wrap gap-2 min-h-[40px]">
{ITEMS.map((item, i) => (
<div
key={item}
style={{
transition: "opacity 300ms ease, transform 300ms ease",
opacity: isVisible(i) ? 1 : 0,
transform: isVisible(i) ? "translateY(0)" : "translateY(8px)",
}}
>
<span className="text-sm px-3 py-1 rounded-full bg-muted border border-border font-medium">
{item}
</span>
</div>
))}
</div>
<Button variant="outline" size="sm" onClick={replay} className="w-fit">
Replay
</Button>
</div>
);
}Installation
Install the following dependencies:
Copy and paste the following code into your project:
import * as React from "react";
interface UseStaggeredRevealOptions {
count: number;
stagger?: number;
initialDelay?: number;
enabled?: boolean;
triggerKey?: string | number;
}
interface UseStaggeredRevealReturn {
getDelay: (index: number) => number;
isVisible: (index: number) => boolean;
replay: () => void;
}
function useStaggeredReveal({
count,
stagger = 60,
initialDelay = 0,
enabled = true,
triggerKey,
}: UseStaggeredRevealOptions): UseStaggeredRevealReturn {
const [visibleCount, setVisibleCount] = React.useState(0);
const [iteration, setIteration] = React.useState(0);
const timeoutsRef = React.useRef<ReturnType<typeof setTimeout>[]>([]);
const clearAllTimeouts = React.useCallback(() => {
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
}, []);
const startReveal = React.useCallback(() => {
clearAllTimeouts();
setVisibleCount(0);
if (!enabled || count === 0) return;
for (let i = 0; i < count; i++) {
const delay = initialDelay + i * stagger;
const timeout = setTimeout(() => {
setVisibleCount((prev) => Math.max(prev, i + 1));
}, delay);
timeoutsRef.current.push(timeout);
}
}, [count, stagger, initialDelay, enabled, clearAllTimeouts]);
React.useEffect(() => {
startReveal();
return clearAllTimeouts;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [iteration, triggerKey]);
React.useEffect(() => {
startReveal();
return clearAllTimeouts;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const replay = React.useCallback(() => {
setIteration((prev) => prev + 1);
}, []);
const getDelay = React.useCallback(
(index: number): number => initialDelay + index * stagger,
[initialDelay, stagger],
);
const isVisible = React.useCallback(
(index: number): boolean => enabled && index < visibleCount,
[enabled, visibleCount],
);
return { getDelay, isVisible, replay };
}
export default useStaggeredReveal;Update the import paths to match your project setup.
Usage
const { getDelay, isVisible, replay } = useStaggeredReveal({
count: items.length,
stagger: 60,
initialDelay: 0,
});Props
| Prop | Type | Default |
|---|---|---|
triggerKey? | string | number | - |
enabled? | boolean | - |
initialDelay? | number | - |
stagger? | number | - |
count | number | - |