shadimage
7ovr
blockactivity-4source: ok · hit

Activity Tabs With Unread

Activity panel with All, Mentions, and Comments tabs, an unread count badge, and rows carrying an avatar, type icon, timestamp, and unread dot.

npx shadcn add "https://7ovr.com/r/activity-4.json"
free

Where it goes

comment

Use cases

community

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@base-ui/react

Registry dependencies

avatarbadgetabs

Files1 · source cached 10/9/2026, 5:42:13 AM

registry/blocks/activity/4/activity-block.tsx
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type IconProps = { className?: string; size?: number | string }

type ActivityType = "mention" | "comment" | "review"

type Item = {
  id: number
  type: ActivityType
  actor: { name: string; initials: string; avatar: string }
  action: string
  target: string
  time: string
  unread: boolean
}

const typeIcon = {
  mention: (p: IconProps) => (
    <IconPlaceholder
      lucide="AtSign"
      tabler="IconAt"
      hugeicons="AtIcon"
      phosphor="At"
      remixicon="RiAtLine"
      {...p}
    />
  ),
  comment: (p: IconProps) => (
    <IconPlaceholder
      lucide="MessageCircle"
      tabler="IconMessageCircle"
      hugeicons="Message01Icon"
      phosphor="ChatCircle"
      remixicon="RiChat3Line"
      {...p}
    />
  ),
  review: (p: IconProps) => (
    <IconPlaceholder
      lucide="GitPullRequest"
      tabler="IconGitPullRequest"
      hugeicons="GitPullRequestIcon"
      phosphor="GitPullRequest"
      remixicon="RiGitPullRequestLine"
      {...p}
    />
  ),
}

const items: Item[] = [
  {
    id: 1,
    type: "mention",
    actor: {
      name: "Mara Lin",
      initials: "ML",
      avatar: "https://i.pravatar.cc/64?img=32",
    },
    action: "mentioned you in",
    target: "Q3 Launch Plan",
    time: "9:42 AM",
    unread: true,
  },
  {
    id: 2,
    type: "review",
    actor: {
      name: "Leo Tanaka",
      initials: "LT",
      avatar: "https://i.pravatar.cc/64?img=12",
    },
    action: "requested your review on",
    target: "PR #418",
    time: "9:15 AM",
    unread: true,
  },
  {
    id: 3,
    type: "comment",
    actor: {
      name: "Priya Nair",
      initials: "PN",
      avatar: "https://i.pravatar.cc/64?img=45",
    },
    action: "commented on",
    target: "Dark Theme Tokens",
    time: "Yesterday",
    unread: false,
  },
  {
    id: 4,
    type: "mention",
    actor: {
      name: "Owen Reyes",
      initials: "OR",
      avatar: "https://i.pravatar.cc/64?img=53",
    },
    action: "mentioned you in",
    target: "Standup Notes",
    time: "Yesterday",
    unread: false,
  },
]

const tabs: { value: string; label: string; filter?: ActivityType }[] = [
  { value: "all", label: "All" },
  { value: "mentions", label: "Mentions", filter: "mention" },
  { value: "comments", label: "Comments", filter: "comment" },
]

function ActivityRow({ item }: { item: Item }) {
  const Icon = typeIcon[item.type]
  return (
    <li className="flex items-start gap-3 px-4 py-3">
      <div className="relative">
        <Avatar className="size-9">
          <AvatarImage
            src={item.actor.avatar}
            alt={item.actor.name}
            className="grayscale"
          />
          <AvatarFallback>{item.actor.initials}</AvatarFallback>
        </Avatar>
        <span className="absolute -right-0.5 -bottom-0.5 flex size-4 items-center justify-center rounded-full bg-background">
          <Icon className="size-3 text-muted-foreground" aria-hidden="true" />
        </span>
      </div>
      <div className="min-w-0 flex-1">
        <p className="text-sm text-pretty">
          <span className="font-medium">{item.actor.name}</span>{" "}
          <span className="text-muted-foreground">{item.action}</span>{" "}
          <span className="font-medium">{item.target}</span>
        </p>
        <span className="text-xs text-muted-foreground">{item.time}</span>
      </div>
      {item.unread && (
        <span
          className="mt-1.5 size-2 shrink-0 rounded-full bg-info"
          role="img"
          aria-label="Unread"
        />
      )}
    </li>
  )
}

function ActivityList({ filter }: { filter?: ActivityType }) {
  const list = filter ? items.filter((item) => item.type === filter) : items
  return (
    <ul className="divide-y divide-border">
      {list.map((item) => (
        <ActivityRow key={item.id} item={item} />
      ))}
    </ul>
  )
}

export default function ActivityBlock() {
  const unread = items.filter((item) => item.unread).length

  return (
    <section className="flex min-h-svh w-full items-center justify-center bg-background px-6 py-16 text-foreground">
      <div className="w-full max-w-md rounded-xl border border-border bg-card ring-1 ring-foreground/5">
        <div className="flex items-center justify-between gap-2 border-b border-border p-4">
          <h2 className="font-heading text-sm font-semibold">Activity</h2>
          <Badge variant="secondary">{unread} New</Badge>
        </div>
        <Tabs defaultValue="all">
          <div className="px-4 pt-3">
            <TabsList className="w-full">
              {tabs.map((tab) => (
                <TabsTrigger key={tab.value} value={tab.value}>
                  {tab.label}
                </TabsTrigger>
              ))}
            </TabsList>
          </div>
          {tabs.map((tab) => (
            <TabsContent key={tab.value} value={tab.value} className="mt-2">
              <ActivityList filter={tab.filter} />
            </TabsContent>
          ))}
        </Tabs>
      </div>
    </section>
  )
}

More from 7ovr

Browse all components in this library →