Bar Chart
Animated bars with hover tooltips — no chart library needed.
Monthly revenue
$461k
JanFebMarAprMayJunJul
components/ui/bar-chart.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
type Datum = { label: string; value: number };
const defaultData: Datum[] = [
{ label: "Jan", value: 42 },
{ label: "Feb", value: 58 },
{ label: "Mar", value: 51 },
{ label: "Apr", value: 74 },
{ label: "May", value: 66 },
{ label: "Jun", value: 89 },
{ label: "Jul", value: 81 },
];
export function BarChart({ data = defaultData, height = 220, format = (v: number) => `$${v}k` }: { data?: Datum[]; height?: number; format?: (v: number) => string }) {
const [hover, setHover] = useState<number | null>(null);
const max = Math.max(...data.map((d) => d.value));
return (
<div className="w-full">
<div className="relative flex items-end gap-3" style={{ height }} onMouseLeave={() => setHover(null)}>
{[0.25, 0.5, 0.75, 1].map((g) => (
<div key={g} aria-hidden className="absolute inset-x-0 border-t border-dashed border-zinc-100" style={{ bottom: `${g * 100}%` }} />
))}
{data.map((d, i) => (
<div key={d.label} className="relative flex h-full flex-1 items-end" onMouseEnter={() => setHover(i)}>
<AnimatePresence>
{hover === i && (
<motion.span
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="absolute left-1/2 z-10 -translate-x-1/2 whitespace-nowrap rounded-md bg-zinc-900 px-2 py-1 text-xs font-medium text-white"
style={{ bottom: `calc(${(d.value / max) * 100}% + 8px)` }}
>
{format(d.value)}
</motion.span>
)}
</AnimatePresence>
<motion.div
initial={{ height: 0 }}
whileInView={{ height: `${(d.value / max) * 100}%` }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: i * 0.06, ease: [0.22, 1, 0.36, 1] }}
className={`w-full rounded-t-lg transition-colors ${hover === null || hover === i ? "bg-blue-600" : "bg-blue-200"}`}
role="img"
aria-label={`${d.label}: ${format(d.value)}`}
/>
</div>
))}
</div>
<div className="mt-2 flex gap-3">
{data.map((d) => (
<span key={d.label} className="flex-1 text-center text-xs text-zinc-500">
{d.label}
</span>
))}
</div>
</div>
);
}
export default function Demo() {
return (
<div className="w-full max-w-xl rounded-2xl border border-zinc-200 bg-white p-6">
<p className="text-sm text-zinc-500">Monthly revenue</p>
<p className="mb-6 text-2xl font-semibold text-zinc-900">$461k</p>
<BarChart />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/bar-chart.tsxcomponents/ui/bar-chart.tsx"use client"; import { AnimatePresence, motion } from "motion/react"; import { useState } from "react"; type Datum = { label: string; value: number }; const defaultData: Datum[] = [ { label: "Jan", value: 42 }, { label: "Feb", value: 58 }, { label: "Mar", value: 51 }, { label: "Apr", value: 74 }, { label: "May", value: 66 }, { label: "Jun", value: 89 }, { label: "Jul", value: 81 }, ]; export function BarChart({ data = defaultData, height = 220, format = (v: number) => `$${v}k` }: { data?: Datum[]; height?: number; format?: (v: number) => string }) { const [hover, setHover] = useState<number | null>(null); const max = Math.max(...data.map((d) => d.value)); return ( <div className="w-full"> <div className="relative flex items-end gap-3" style={{ height }} onMouseLeave={() => setHover(null)}> {[0.25, 0.5, 0.75, 1].map((g) => ( <div key={g} aria-hidden className="absolute inset-x-0 border-t border-dashed border-zinc-100" style={{ bottom: `${g * 100}%` }} /> ))} {data.map((d, i) => ( <div key={d.label} className="relative flex h-full flex-1 items-end" onMouseEnter={() => setHover(i)}> <AnimatePresence> {hover === i && ( <motion.span initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} className="absolute left-1/2 z-10 -translate-x-1/2 whitespace-nowrap rounded-md bg-zinc-900 px-2 py-1 text-xs font-medium text-white" style={{ bottom: `calc(${(d.value / max) * 100}% + 8px)` }} > {format(d.value)} </motion.span> )} </AnimatePresence> <motion.div initial={{ height: 0 }} whileInView={{ height: `${(d.value / max) * 100}%` }} viewport={{ once: true }} transition={{ duration: 0.8, delay: i * 0.06, ease: [0.22, 1, 0.36, 1] }} className={`w-full rounded-t-lg transition-colors ${hover === null || hover === i ? "bg-blue-600" : "bg-blue-200"}`} role="img" aria-label={`${d.label}: ${format(d.value)}`} /> </div> ))} </div> <div className="mt-2 flex gap-3"> {data.map((d) => ( <span key={d.label} className="flex-1 text-center text-xs text-zinc-500"> {d.label} </span> ))} </div> </div> ); } export default function Demo() { return ( <div className="w-full max-w-xl rounded-2xl border border-zinc-200 bg-white p-6"> <p className="text-sm text-zinc-500">Monthly revenue</p> <p className="mb-6 text-2xl font-semibold text-zinc-900">$461k</p> <BarChart /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.