Activity feed
Who did what and when, with initials and a quiet timestamp.
npx shadcn@latest add https://ui.goodtabs.co/r/activity-feed.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/activity-feed
Maya Chen merged Dock polish
Omar Reyes opened Table sorting
Readable at 250px.
Team feed
Maya Chen merged Dock polish into main
Omar Reyes commented on Table sorting
Priya Nair published Release 1.4
Jonas Weber archived Old pricing page
Four events in a bordered box.
Usage
import { ActivityFeed, ActivityItem } from "@/components/ui/activity-feed"
<ActivityFeed>
<ActivityItem actor="Maya Chen" action="merged" target="Dock polish" time="2h ago" dateTime="2026-11-05T09:30:00Z" />
</ActivityFeed>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| actor | string | ActivityItem: who did it. Initials come from this. | |
| action | string | ActivityItem: the verb, for example merged. | |
| target | string | ActivityItem: what it was done to. | |
| time | string | ActivityItem: the time as people read it. | |
| dateTime | string | ActivityItem: machine-readable time. |
Source
activity-feed/activity-feed.tsx
import * as React from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
type ActivityFeedProps = React.ComponentProps<"ul">;
/** Who did what, newest first. Compose with ActivityItem. */
export function ActivityFeed({ className, ...props }: ActivityFeedProps) {
return <ul className={cn("m-0 flex list-none flex-col gap-4 p-0", className)} {...props} />;
}
type ActivityItemProps = React.ComponentProps<"li"> & {
/** Who did it. The initials are taken from this. */
actor: string;
/** What they did, for example "merged". */
action: string;
/** What it was done to, shown in the foreground tone. */
target: string;
/** Machine-readable time, for example 2026-11-05T09:30:00Z. */
dateTime: string;
/** The time as people read it, for example "2 hours ago". */
time: string;
};
function initials(name: string) {
return name
.split(/\s+/)
.slice(0, 2)
.map((part) => Array.from(part)[0]?.toUpperCase() ?? "")
.join("");
}
export function ActivityItem({ actor, action, target, dateTime, time, className, ...props }: ActivityItemProps) {
return (
<li className={cn("flex items-start gap-3", className)} {...props}>
<Avatar size="sm">
<AvatarFallback aria-hidden="true">{initials(actor)}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col gap-0.5">
<p className="m-0 text-sm text-muted-foreground">
<span className="text-foreground">{actor}</span> {action} <span className="text-foreground">{target}</span>
</p>
<time dateTime={dateTime} className="font-mono text-xs text-muted-foreground">
{time}
</time>
</div>
</li>
);
}
For AI agents
Hand your agent this page, the markdown at /components/activity-feed/prompt.md, or the Copy AI prompt button above.
When to use
- Recent activity on a project, workspace or record
- Audit trails with a short sentence per event
When not to use
- Dated milestones: use timeline
- Long conversations: use a thread
Agent notes
- Format the relative time yourself; the component does not know about locales
- Initials only, no images, so it works without data
- Pass items newest first