Changelog
Stay up to date with the latest features and improvements
NewAI-Powered Analytics Dashboard
Get intelligent insights and recommendations based on your data patterns using advanced machine learning.
NewTeam Workspaces
Create dedicated workspaces for different teams with custom permissions and settings.
ImprovedFaster Page Load Times
Optimized performance with 40% faster page loads and improved caching.
ImprovedEnhanced Search
Improved search algorithm with fuzzy matching and instant results.
FixedFixed Export Issues
Resolved issues with CSV exports containing special characters.
Installation
Install dependencies
npm install framer-motion lucide-reactCopy and paste the following code into your project.
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Sparkles, Bug, Zap, Plus, ChevronDown } from "lucide-react";
interface ChangelogEntry {
id: string;
version: string;
date: string;
title: string;
description?: string;
changes: {
type: "feature" | "improvement" | "bugfix" | "breaking";
title: string;
description?: string;
}[];
}
interface ChangelogProps {
title?: string;
description?: string;
entries: ChangelogEntry[];
className?: string;
}
export function Changelog({
title = "Changelog",
description = "Stay up to date with the latest features and improvements",
entries,
className = "",
}: ChangelogProps) {
const [expandedEntries, setExpandedEntries] = useState<Set<string>>(
new Set([entries[0]?.id]),
);
const toggleEntry = (id: string) => {
const newExpanded = new Set(expandedEntries);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedEntries(newExpanded);
};
const getChangeTypeConfig = (type: ChangelogEntry["changes"][0]["type"]) => {
switch (type) {
case "feature":
return {
label: "New",
color: "text-blue-600 dark:text-blue-400",
bg: "bg-blue-50 dark:bg-blue-950/30",
icon: Plus,
};
case "improvement":
return {
label: "Improved",
color: "text-emerald-600 dark:text-emerald-400",
bg: "bg-emerald-50 dark:bg-emerald-950/30",
icon: Zap,
};
case "bugfix":
return {
label: "Fixed",
color: "text-amber-600 dark:text-amber-400",
bg: "bg-amber-50 dark:bg-amber-950/30",
icon: Bug,
};
case "breaking":
return {
label: "Breaking",
color: "text-red-600 dark:text-red-400",
bg: "bg-red-50 dark:bg-red-950/30",
icon: Sparkles,
};
}
};
return (
<section className={`w-full py-24 ${className}`}>
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="mb-16 text-center"
>
<h2 className="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-5xl">
{title}
</h2>
<p className="mx-auto max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
{description}
</p>
</motion.div>
<div className="space-y-6">
{entries.map((entry, index) => {
const isExpanded = expandedEntries.has(entry.id);
const isLatest = index === 0;
return (
<motion.div
key={entry.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
delay: index * 0.1,
ease: "easeOut",
}}
>
<div
className={`overflow-hidden rounded-2xl border bg-white shadow-sm transition-all duration-300 dark:bg-zinc-900 ${
isLatest
? "border-blue-200 ring-2 ring-blue-100 dark:border-blue-800 dark:ring-blue-950/50"
: "border-zinc-200 dark:border-zinc-800"
}`}
>
<button
onClick={() => toggleEntry(entry.id)}
className="flex w-full items-center justify-between p-6 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<div className="flex-1">
<div className="mb-2 flex items-center gap-3">
<span className="inline-flex items-center gap-1.5 rounded-full bg-zinc-100 px-3 py-1 text-sm font-semibold text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300">
v{entry.version}
</span>
{isLatest && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-600 dark:bg-blue-950/30 dark:text-blue-400">
<Sparkles className="h-3 w-3" />
Latest
</span>
)}
<span className="text-sm text-zinc-500 dark:text-zinc-400">
{entry.date}
</span>
</div>
<h3 className="text-xl font-bold text-zinc-900 dark:text-white">
{entry.title}
</h3>
{entry.description && (
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{entry.description}
</p>
)}
</div>
<motion.div
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.2 }}
className="ml-4"
>
<ChevronDown className="h-5 w-5 text-zinc-400" />
</motion.div>
</button>
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="overflow-hidden"
>
<div className="border-t border-zinc-100 bg-zinc-50/50 p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
<div className="space-y-4">
{entry.changes.map((change, idx) => {
const config = getChangeTypeConfig(change.type);
const Icon = config.icon;
return (
<motion.div
key={idx}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{
delay: idx * 0.05,
duration: 0.3,
ease: "easeOut",
}}
className="flex gap-3"
>
<div
className={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg ${config.bg}`}
>
<Icon
className={`h-4 w-4 ${config.color}`}
/>
</div>
<div className="flex-1">
<div className="mb-1 flex items-center gap-2">
<span
className={`text-xs font-semibold ${config.color}`}
>
{config.label}
</span>
<span className="font-medium text-zinc-900 dark:text-white">
{change.title}
</span>
</div>
{change.description && (
<p className="text-sm text-zinc-600 dark:text-zinc-400">
{change.description}
</p>
)}
</div>
</motion.div>
);
})}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
})}
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4, ease: "easeOut" }}
className="mt-12 text-center"
>
<a
href="#"
className="inline-flex items-center gap-2 text-sm font-semibold text-blue-600 transition-colors hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
View full changelog
<ChevronDown className="h-4 w-4 -rotate-90" />
</a>
</motion.div>
</div>
</section>
);
}Features
- ✅ Collapsible entries - Click to expand/collapse each version
- ✅ Latest badge - Highlights the newest release
- ✅ Color-coded changes - Different colors for each change type
- ✅ Change type icons - Visual indicators for each change
- ✅ Smooth animations - Expand/collapse with smooth transitions
- ✅ Clean design - Modern card-based layout
- ✅ Responsive - Works beautifully on all screen sizes
- ✅ Dark mode ready - Elegant in both themes
- ✅ TypeScript support - Full type safety
Usage
Basic Usage
import Changelog from "@/components/ui/changelog";
const changelogEntries = [
{
id: "v2.5.0",
version: "2.5.0",
date: "November 15, 2024",
title: "AI-Powered Analytics",
description: "Major update with AI-powered insights.",
changes: [
{
type: "feature",
title: "AI Analytics Dashboard",
description: "Get intelligent insights based on your data patterns.",
},
{
type: "improvement",
title: "Faster Page Loads",
description: "40% faster page loads with improved caching.",
},
{
type: "bugfix",
title: "Fixed Export Issues",
description: "Resolved CSV export problems.",
},
],
},
];
export default function Page() {
return <Changelog entries={changelogEntries} />;
}Custom Title and Description
<Changelog
title="What's New"
description="See what we've been working on"
entries={changelogEntries}
/>Props
ChangelogProps
| Prop | Type | Default | Description |
|---|---|---|---|
entries | ChangelogEntry[] | Required | Array of changelog entries |
title | string | "Changelog" | Section title |
description | string | "Stay up to date with..." | Section description |
className | string | "" | Additional CSS classes |
ChangelogEntry
| Prop | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
version | string | Yes | Version number (e.g., "2.5.0") |
date | string | Yes | Release date |
title | string | Yes | Release title |
description | string | No | Release description |
changes | Change[] | Yes | Array of changes |
Change
| Prop | Type | Required | Description |
|---|---|---|---|
type | "feature" | "improvement" | "bugfix" | "breaking" | Yes | Type of change |
title | string | Yes | Change title |
description | string | No | Change description |
TypeScript Interface
interface ChangelogEntry {
id: string;
version: string;
date: string;
title: string;
description?: string;
changes: {
type: "feature" | "improvement" | "bugfix" | "breaking";
title: string;
description?: string;
}[];
}
interface ChangelogProps {
title?: string;
description?: string;
entries: ChangelogEntry[];
className?: string;
}Use Cases
Perfect for:
- Product changelog pages
- Release notes
- Update announcements
- Version history
- Developer documentation
- Customer communication
- Transparency pages
- What's new sections
Customization
Change Colors
// In the component, modify getChangeTypeConfig function
case "feature":
return {
label: "New",
color: "text-purple-600 dark:text-purple-400", // Your brand color
bg: "bg-purple-50 dark:bg-purple-950/30",
icon: Plus,
};Add Custom Change Types
// Add a new change type
type ChangeType = "feature" | "improvement" | "bugfix" | "breaking" | "security";
// Add to getChangeTypeConfig
case "security":
return {
label: "Security",
color: "text-violet-600 dark:text-violet-400",
bg: "bg-violet-50 dark:bg-violet-950/30",
icon: Shield,
};Change Default Expanded State
// Expand all entries by default
const [expandedEntries, setExpandedEntries] = useState<Set<string>>(
new Set(entries.map((e) => e.id))
);
// Collapse all by default
const [expandedEntries, setExpandedEntries] = useState<Set<string>>(new Set());
// Expand first 3 entries
const [expandedEntries, setExpandedEntries] = useState<Set<string>>(
new Set(entries.slice(0, 3).map((e) => e.id))
);Remove Latest Badge
// If you don't want the "Latest" badge
// Remove or comment out this section:
{
isLatest && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-3 py-1 text-xs font-semibold text-blue-600">
<Sparkles className="h-3 w-3" />
Latest
</span>
);
}Change Animation Speed
// Make animations faster or slower
<motion.div
initial={{height: 0, opacity: 0}}
animate={{height: "auto", opacity: 1}}
exit={{height: 0, opacity: 0}}
transition={{duration: 0.5}} // Slower: 0.5, Faster: 0.2
>Common Patterns
SaaS Product Changelog
const saasChangelog = [
{
id: "v3.0.0",
version: "3.0.0",
date: "December 1, 2024",
title: "Major Platform Upgrade",
description: "Complete redesign with new features and improvements.",
changes: [
{
type: "feature",
title: "New Dashboard",
description: "Completely redesigned dashboard with better UX.",
},
{
type: "feature",
title: "Advanced Analytics",
description: "AI-powered insights and predictions.",
},
{
type: "breaking",
title: "API v2 Required",
description: "API v1 is deprecated. Please upgrade to v2.",
},
],
},
];Mobile App Changelog
const mobileChangelog = [
{
id: "v1.5.0",
version: "1.5.0",
date: "November 20, 2024",
title: "Performance & Stability",
changes: [
{
type: "improvement",
title: "Faster App Launch",
description: "App now launches 50% faster.",
},
{
type: "bugfix",
title: "Fixed Crash on iOS 18",
description: "Resolved crash when opening notifications.",
},
],
},
];API Changelog
const apiChangelog = [
{
id: "v2.1.0",
version: "2.1.0",
date: "November 10, 2024",
title: "New Endpoints & Rate Limits",
changes: [
{
type: "feature",
title: "Webhooks API",
description: "New endpoints for managing webhooks.",
},
{
type: "improvement",
title: "Increased Rate Limits",
description: "Rate limits doubled for all paid plans.",
},
{
type: "breaking",
title: "Authentication Changes",
description: "OAuth 2.0 now required for all endpoints.",
},
],
},
];Integration Examples
With RSS Feed
// Generate RSS feed from changelog
export async function GET() {
const feed = new RSS({
title: "Product Changelog",
description: "Latest updates",
feed_url: "https://example.com/changelog/rss",
site_url: "https://example.com",
});
changelogEntries.forEach((entry) => {
feed.item({
title: `v${entry.version}: ${entry.title}`,
description: entry.description,
url: `https://example.com/changelog#${entry.id}`,
date: entry.date,
});
});
return new Response(feed.xml(), {
headers: {"Content-Type": "application/xml"},
});
}With Email Notifications
// Send email when new version is released
async function notifyUsers(entry: ChangelogEntry) {
await sendEmail({
to: subscribers,
subject: `New Update: v${entry.version}`,
html: `
<h1>${entry.title}</h1>
<p>${entry.description}</p>
<ul>
${entry.changes.map((c) => `<li>${c.title}</li>`).join("")}
</ul>
`,
});
}With Analytics
// Track which changelog entries users view
const trackChangelogView = (version: string) => {
analytics.track("changelog_entry_viewed", {
version,
timestamp: Date.now(),
});
};
// Track which entries users expand
const trackChangelogExpand = (version: string) => {
analytics.track("changelog_entry_expanded", {
version,
timestamp: Date.now(),
});
};