Changelog
Release notes timeline with New / Improved / Fixed tags.
v2.4
Nov 12
Command palette
New- • Press ⌘K anywhere to search
- • Keyboard navigation for every result
v2.3
Oct 28
Faster dashboards
Improved- • Charts render 3× faster
- • Lazy-loaded widgets
v2.2
Oct 09
Export fixes
Fixed- • CSV exports keep time zones
- • PDF page breaks no longer cut tables
components/ui/changelog.tsx
"use client";
import { motion } from "motion/react";
type Release = { version: string; date: string; title: string; tag: "New" | "Improved" | "Fixed"; items: string[] };
const releases: Release[] = [
{ version: "v2.4", date: "Nov 12", title: "Command palette", tag: "New", items: ["Press ⌘K anywhere to search", "Keyboard navigation for every result"] },
{ version: "v2.3", date: "Oct 28", title: "Faster dashboards", tag: "Improved", items: ["Charts render 3× faster", "Lazy-loaded widgets"] },
{ version: "v2.2", date: "Oct 09", title: "Export fixes", tag: "Fixed", items: ["CSV exports keep time zones", "PDF page breaks no longer cut tables"] },
];
const tagStyle = { New: "bg-blue-50 text-blue-700", Improved: "bg-emerald-50 text-emerald-700", Fixed: "bg-amber-50 text-amber-700" };
export function Changelog({ items = releases }: { items?: Release[] }) {
return (
<ol className="space-y-10">
{items.map((r, i) => (
<motion.li
key={r.version}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1 }}
className="grid gap-3 sm:grid-cols-[120px_1fr]"
>
<div className="text-sm">
<p className="font-mono font-medium text-zinc-900">{r.version}</p>
<p className="text-zinc-400">{r.date}</p>
</div>
<div className="border-l border-zinc-200 pl-6">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-zinc-900">{r.title}</h3>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${tagStyle[r.tag]}`}>{r.tag}</span>
</div>
<ul className="mt-2 space-y-1 text-sm text-zinc-500">
{r.items.map((it) => (
<li key={it}>• {it}</li>
))}
</ul>
</div>
</motion.li>
))}
</ol>
);
}
export default function Demo() {
return (
<div className="w-full max-w-xl">
<Changelog />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/changelog.tsxcomponents/ui/changelog.tsx"use client"; import { motion } from "motion/react"; type Release = { version: string; date: string; title: string; tag: "New" | "Improved" | "Fixed"; items: string[] }; const releases: Release[] = [ { version: "v2.4", date: "Nov 12", title: "Command palette", tag: "New", items: ["Press ⌘K anywhere to search", "Keyboard navigation for every result"] }, { version: "v2.3", date: "Oct 28", title: "Faster dashboards", tag: "Improved", items: ["Charts render 3× faster", "Lazy-loaded widgets"] }, { version: "v2.2", date: "Oct 09", title: "Export fixes", tag: "Fixed", items: ["CSV exports keep time zones", "PDF page breaks no longer cut tables"] }, ]; const tagStyle = { New: "bg-blue-50 text-blue-700", Improved: "bg-emerald-50 text-emerald-700", Fixed: "bg-amber-50 text-amber-700" }; export function Changelog({ items = releases }: { items?: Release[] }) { return ( <ol className="space-y-10"> {items.map((r, i) => ( <motion.li key={r.version} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: i * 0.1 }} className="grid gap-3 sm:grid-cols-[120px_1fr]" > <div className="text-sm"> <p className="font-mono font-medium text-zinc-900">{r.version}</p> <p className="text-zinc-400">{r.date}</p> </div> <div className="border-l border-zinc-200 pl-6"> <div className="flex items-center gap-2"> <h3 className="font-semibold text-zinc-900">{r.title}</h3> <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${tagStyle[r.tag]}`}>{r.tag}</span> </div> <ul className="mt-2 space-y-1 text-sm text-zinc-500"> {r.items.map((it) => ( <li key={it}>• {it}</li> ))} </ul> </div> </motion.li> ))} </ol> ); } export default function Demo() { return ( <div className="w-full max-w-xl"> <Changelog /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.