Stacked bars
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Hover a column for its breakdown; switch Day and Week to restack the bars.
Code
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState } from "react";
import { rng, Surface } from "../ui";
const SERIES = [
{ name: "Direct", color: "#e5493a" },
{ name: "Search", color: "#8a6ff0" },
{ name: "Social", color: "#4c7bf0" },
{ name: "Email", color: "#9db3f2" },
{ name: "Ads", color: "#f2c14e" },
];
function make(seed: number, n: number, scale: number) {
const r = rng(seed);
return Array.from({ length: n }, (_, i) => SERIES.map((_, k) => Math.round((k === 0 ? 4 + r() * 3 : 6 + r() * 16 + Math.sin(i * 0.7 + k) * 5) * scale)));
}
const DATA = { Day: make(3, 20, 1), Week: make(8, 20, 6.4) };
export default function StackedBars() {
const [range, setRange] = useState<"Day" | "Week">("Day");
const [hot, setHot] = useState<number | null>(null);
const uid = useId();
const data = DATA[range];
const max = Math.max(...data.map((col) => col.reduce((a, b) => a + b, 0)));
const total = data.reduce((a, col) => a + col.reduce((x, y) => x + y, 0), 0) * 41;
return (
<Surface>
<div className="w-[22em] rounded-[1.2em] bg-white p-[1.1em] shadow-[0_12px_32px_-18px_rgba(0,0,0,0.35)]">
<div className="flex items-start justify-between">
<div>
<p className="text-[0.85em] font-medium">Traffic</p>
<p className="text-[0.7em] text-[#8a8984]">Last 20 {range === "Day" ? "days" : "weeks"}</p>
</div>
<div className="relative flex rounded-full bg-[#f1f0ec] p-[0.2em] text-[0.72em]">
{(["Day", "Week"] as const).map((r) => (
<button key={r} type="button" onClick={() => setRange(r)} className={`relative rounded-full px-[0.8em] py-[0.25em] ${range === r ? "" : "text-[#8a8984]"}`}>
{range === r && <motion.span layoutId={`${uid}-pill`} className="absolute inset-0 rounded-full bg-white shadow-sm" />}
<span className="relative">{r}</span>
</button>
))}
</div>
</div>
<div className="mt-[0.5em] flex items-baseline gap-[0.4em]">
<motion.span key={range} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="text-[1.7em] font-medium tabular-nums">
{total.toLocaleString("en-US")}
</motion.span>
<span className="text-[0.75em] text-[#8a8984]">visits</span>
</div>
<div className="relative mt-[0.6em] flex h-[7.5em] items-end gap-[0.3em]" onPointerLeave={() => setHot(null)}>
{data.map((col, i) => (
<div
key={i}
className="relative flex h-full flex-1 cursor-pointer flex-col-reverse gap-[0.15em]"
onPointerEnter={() => setHot(i)}
onPointerDown={() => setHot(i)}
>
{col.map((v, k) => (
<motion.div
key={k}
className="w-full rounded-full"
style={{ backgroundColor: SERIES[k].color }}
initial={false}
animate={{ height: `${(v / max) * 86}%`, opacity: hot === null || hot === i ? 1 : 0.28 }}
transition={{ type: "spring", stiffness: 260, damping: 26, delay: i * 0.012 + k * 0.01 }}
/>
))}
</div>
))}
<AnimatePresence>
{hot !== null && (
<motion.div
key="tip"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0, left: `${((hot + 0.5) / data.length) * 100}%` }}
exit={{ opacity: 0 }}
transition={{ type: "spring", stiffness: 400, damping: 34 }}
className="pointer-events-none absolute top-0 z-10 rounded-[0.6em] bg-[#151515] px-[0.7em] py-[0.45em] text-[0.62em] text-white"
style={{ translateX: hot > 14 ? "-90%" : hot < 4 ? "-10%" : "-50%" }}
>
{SERIES.map((s, k) => (
<div key={s.name} className="flex items-center gap-[0.5em] whitespace-nowrap">
<span className="h-[0.6em] w-[0.6em] rounded-full" style={{ background: s.color }} />
<span className="flex-1">{s.name}</span>
<span className="tabular-nums">{data[hot][k]}</span>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
<div className="mt-[0.4em] flex justify-between text-[0.65em] text-[#8a8984]">
<span>{range === "Day" ? "Sep 22" : "Jun 2"}</span>
<span>{range === "Day" ? "Oct 11" : "Oct 13"}</span>
</div>
</div>
</Surface>
);
}
Prompt
Build "Stacked bars", a other animation. What it does: Hover a column for its breakdown; switch Day and Week to restack the bars. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).