shadimage
motion-lexicon
uianimated-chartsource: ok · hit

Animated chart

Compares data ranges and series changes while keeping labels and focus stable.

npx shadcn add "https://motion-lexicon.pages.dev/r/animated-chart.json"
free

Where it goes

data-table

Use cases

dashboard

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Files1 · source cached 10/9/2026, 11:08:41 PM

src/registry/components/animated-chart.tsx
"use client";

import { useId, useState } from "react";
import { motion, useReducedMotion } from "motion/react";

export type AnimatedChartSeries = { id: string; label: string; values: readonly number[]; color?: string };
export type AnimatedChartProps = { series: readonly AnimatedChartSeries[]; labels?: readonly string[]; label?: string; className?: string };

export function AnimatedChart({ series, labels = [], label = "Performance chart", className = "" }: AnimatedChartProps) {
  const reduced = useReducedMotion() === true; const titleId = useId(); const [active, setActive] = useState(0); const selected = series[active]; const max = Math.max(1, ...(selected?.values ?? [1])); const points = (selected?.values ?? []).map((value, index, values) => `${values.length < 2 ? 50 : (index / (values.length - 1)) * 100},${92 - (value / max) * 74}`).join(" ");
  return <section aria-labelledby={titleId} className={`rounded-[16px] border border-black/[.1] bg-white p-4 shadow-[0_14px_33px_-26px_rgba(27,27,27,.35)] ${className}`}><div className="flex items-start justify-between gap-3"><div><span className="font-mono text-[10px] uppercase tracking-[.16em] text-neutral-500">Analytics</span><h3 id={titleId} className="mt-1 text-[15px] font-medium text-[#292929]">{label}</h3></div><div role="tablist" aria-label={label} className="flex rounded-full bg-neutral-100 p-1">{series.map((item, index) => <button key={item.id} type="button" role="tab" aria-selected={index === active} onClick={() => setActive(index)} onKeyDown={(event) => { if (event.key === "ArrowRight") { event.preventDefault(); setActive((value) => Math.min(series.length - 1, value + 1)); } if (event.key === "ArrowLeft") { event.preventDefault(); setActive((value) => Math.max(0, value - 1)); } }} className={`min-h-9 rounded-full px-3 text-[10px] outline-none focus-visible:ring-2 focus-visible:ring-[#4568FF] ${index === active ? "bg-[#242424] text-white" : "text-neutral-500"}`}>{item.label}</button>)}</div></div><svg role="img" aria-label={`${selected?.label ?? ""} ${label}`} viewBox="0 0 100 100" preserveAspectRatio="none" className="mt-5 h-40 w-full overflow-visible"><path d="M0 92H100" stroke="#e5e5e5" strokeWidth=".7" /><path d="M0 55H100" stroke="#ececec" strokeWidth=".7" strokeDasharray="2 3" /><motion.polyline key={selected?.id} points={points} fill="none" stroke={selected?.color ?? "#4568FF"} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" initial={reduced ? false : { pathLength: 0, opacity: .4 }} animate={{ pathLength: 1, opacity: 1 }} transition={{ duration: reduced ? 0 : .54, ease: [0.22, 1, 0.36, 1] }} /><circle cx={(selected?.values.length ?? 1) < 2 ? 50 : 100} cy={selected?.values.length ? 92 - (selected.values[selected.values.length - 1] / max) * 74 : 92} r="2.8" fill={selected?.color ?? "#4568FF"} /></svg><div className="mt-2 flex justify-between font-mono text-[9px] text-neutral-400">{(labels.length ? labels : selected?.values.map((_, index) => `P${index + 1}`) ?? []).map((item) => <span key={item}>{item}</span>)}</div></section>;
}

More from motion-lexicon

Browse all components in this library →