Changelog section
A dated release log with a version, a title and short notes per entry.
npx shadcn@latest add https://ui.goodtabs.co/r/changelog-section.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/changelog-section
Changelog
What shipped
- v2.4
Keyboard first tables
- Arrow keys move between cells
- Row selection keeps focus after a sort
- v2.3
Dark mode tokens
- Hairlines are translucent in both themes
- v2.2
Faster command palette
A heading and three releases, newest first.
Dates and titles only
Table cells take focus with the arrow keys
Translucent hairlines in dark mode
Command palette opens in under 50ms
New date range picker
Entries without notes or versions.
Full width
Changelog
Every release, newest first
- v2.4
Keyboard first tables
- Arrow keys move between cells
- Row selection keeps focus after a sort
- v2.3
Dark mode tokens
- Hairlines are translucent in both themes
- v2.2
Faster command palette
- v2.1
Form fields
- Inline errors announce politely
- Labels stay visible while typing
- Number fields clamp on blur
Wider column, four releases and a longer list of notes.
Usage
import { ChangelogSection } from "@/components/changelog-section"
<ChangelogSection
eyebrow="Changelog"
title="What shipped"
entries={[{ date: "2026-11-03", version: "v2.4", title: "Keyboard first tables", items: ["Arrow keys move between cells"] }]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| eyebrow | string | Small uppercase line above the title. | |
| title | ReactNode | The section heading. | |
| entries | { date: string; version?: string; title: string; items?: string[] }[] | Newest first. date is ISO and also sets the time element. | |
| size | "sm" | "default" | "default" | Spacing scale. sm fits a card. |
Source
changelog-section/changelog-section.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
export type ChangelogEntry = {
/** ISO date, for example 2026-11-05. Drives the label and the machine readable time. */
date: string;
/** Version or release name, shown in mono next to the date. */
version?: string;
/** What shipped, in a few words. */
title: string;
/** Short bullet lines. */
items?: string[];
};
type ChangelogSectionProps = Omit<React.ComponentProps<"section">, "title"> & {
/** Small uppercase line above the title. */
eyebrow?: string;
/** The section heading. */
title?: React.ReactNode;
/** Newest first. */
entries: ChangelogEntry[];
/** Spacing scale. "sm" fits a card or narrow column. */
size?: "sm" | "default";
};
const formatter = new Intl.DateTimeFormat("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
timeZone: "UTC",
});
function formatDate(iso: string) {
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? iso : formatter.format(d);
}
/**
* A dated release log: a heading, then one row per release with the date on the left and the notes on the right.
* Server component. Entries are data, so nothing here needs state.
*/
export function ChangelogSection({
eyebrow,
title,
entries,
size = "default",
className,
...props
}: ChangelogSectionProps) {
const sm = size === "sm";
return (
<section className={cn(sm ? "py-8" : "py-16 lg:py-24", className)} {...props}>
<div className={cn("mx-auto w-full px-4 md:px-6", sm ? "max-w-xl" : "max-w-3xl")}>
{(eyebrow || title) && (
<header className={cn("flex flex-col gap-2", sm ? "mb-6" : "mb-10")}>
{eyebrow && (
<p className="m-0 font-mono text-xs tracking-wide text-muted-foreground uppercase">{eyebrow}</p>
)}
{title && (
<h2 className={cn("m-0 font-medium tracking-tight text-balance", sm ? "text-xl" : "text-3xl")}>
{title}
</h2>
)}
</header>
)}
<ol className="m-0 flex list-none flex-col p-0">
{entries.map((entry) => (
<li
key={`${entry.date}-${entry.title}`}
className={cn(
"grid gap-x-8 gap-y-2 border-t py-5 first:border-t-0 first:pt-0 sm:grid-cols-[8rem_1fr]",
sm && "sm:grid-cols-[6.5rem_1fr]",
)}
>
<div className="flex flex-row items-baseline gap-2 sm:flex-col sm:gap-0.5">
<time dateTime={entry.date} className="font-mono text-xs text-muted-foreground">
{formatDate(entry.date)}
</time>
{entry.version && <span className="font-mono text-xs text-foreground/70">{entry.version}</span>}
</div>
<div className="flex min-w-0 flex-col gap-2">
<h3 className="m-0 text-base font-medium tracking-tight">{entry.title}</h3>
{entry.items && entry.items.length > 0 && (
<ul className="m-0 flex list-none flex-col gap-1 p-0 text-sm text-muted-foreground">
{entry.items.map((item) => (
<li key={item} className="flex gap-2">
<span aria-hidden="true" className="mt-2 h-px w-2 shrink-0 bg-border" />
<span>{item}</span>
</li>
))}
</ul>
)}
</div>
</li>
))}
</ol>
</div>
</section>
);
}
For AI agents
Hand your agent this page, the markdown at /components/changelog-section/prompt.md, or the Copy AI prompt button above.
When to use
- A public release log on a product or docs site
- When dates matter and readers scan for what is new
When not to use
- For one announcement, use an announcement bar
- For long release notes with screenshots, link to a post
Agent notes
- Server component with no state
- Dates are ISO strings formatted in UTC as 5 Nov 2026, so server and client agree
- The section carries py-16 lg:py-24 and a max-w-3xl column; place it directly in main