Staggered Bar Chart
A simple animated bar chart for Next.js. The bars animate in when entering the screen, and it will shows a tooltip when the bar is hovered.
- Next.js
- Typescript
- GSAP
- ScrollTrigger Plugin
- useGSAP Plugin
- Tailwind CSS
components/staggered-bar-chart.tsx
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useRef, useState } from "react";
gsap.registerPlugin(useGSAP, ScrollTrigger);
export function StaggeredBarChart({
data,
maxHeight = 250,
}: {
data: Array<number>;
maxHeight?: number;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const barRefs = useRef<Array<HTMLDivElement>>([]);
const [hoveredData, setHoveredData] = useState<number | null>(null);
const [isHoveringBars, setIsHoveringBars] = useState(false);
useGSAP(
() => {
const bars = barRefs.current;
gsap.from(bars, {
scaleY: 0,
transformOrigin: "50% 100%",
stagger: 0.015,
ease: "power1.inOut",
duration: 0.3,
delay: 0.5,
scrollTrigger: {
trigger: containerRef.current,
start: "top 90%",
},
});
},
{ scope: containerRef }
);
// linear normalization
const maxValue = Math.max(...data);
const scaledData = data.map((value) => (value / maxValue) * maxHeight);
const handleMouseEnter = (index: number) => {
setHoveredData(data[index]);
setIsHoveringBars(true);
const bar = barRefs.current[index];
gsap.to(bar, {
scaleX: 2,
opacity: 1,
ease: "power1.out",
duration: 0.5,
});
const tooltip = tooltipRef.current;
if (!tooltip) return;
if (!isHoveringBars) {
gsap.set(tooltipRef.current, {
scale: 0.1,
opacity: 0,
y: -scaledData[index],
x: bar.offsetLeft - 24 - tooltip.offsetWidth / 2,
ease: "power4.out",
});
}
gsap.to(tooltipRef.current, {
transformOrigin: "50% 100%",
scale: 1,
y: -scaledData[index] - 12,
x: bar.offsetLeft - 24 - tooltip.offsetWidth / 2,
opacity: 1,
duration: 0.5,
ease: "power4.out",
});
};
const handleMouseLeave = (index: number) => {
const bar = barRefs.current[index];
gsap.to(bar, {
scaleX: 1,
opacity: 0.2,
ease: "power1.out",
duration: 0.4,
delay: 0.2,
});
const tooltip = tooltipRef.current;
if (!tooltip) return;
setIsHoveringBars(false);
gsap.to(tooltipRef.current, {
scale: 0.1,
opacity: 0,
y: -scaledData[index],
x: bar.offsetLeft - 24 - tooltip.offsetWidth / 2,
ease: "power4.out",
});
};
return (
<div
ref={containerRef}
className="flex items-end bg-texture w-fit p-6 rounded relative"
>
{data.map((item, index) => (
<div
key={index}
className="bar-container w-3 overflow-hidden"
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
>
<div
className="bar w-0.5 bg-blue-600 opacity-30 mx-auto will-change-[width]"
style={{ height: scaledData[index] }}
ref={(el) => {
if (el) barRefs.current[index] = el;
}}
></div>
</div>
))}
<div
ref={tooltipRef}
className="bg-blue-600 px-3 py-1 rounded-full text-white font-sans text-xs absolute pointer-events-none opacity-0"
>
{hoveredData}
</div>
</div>
);
}
<StaggeredBarChart
data={[
20, 28, 54, 120, 124, 110, 102, 149, 240, 270, 298, 340, 380, 390, 405, 415,
402, 390, 375, 366, 356, 362, 390, 410, 422, 434, 445, 467
]}
/>