Bar Chart

Animated bars with hover tooltips — no chart library needed.

Monthly revenue

$461k

JanFebMarAprMayJunJul

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/bar-chart.tsx

    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>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.