Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
actorstringActivityItem: who did it. Initials come from this.
actionstringActivityItem: the verb, for example merged.
targetstringActivityItem: what it was done to.
timestringActivityItem: the time as people read it.
dateTimestringActivityItem: 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