# Live Activity Feed — React Dashboard Widget

> A feed where new events drop in at the top on a timer with a brief highlight while older rows slide down and the last one fades out; pauses on hover.

- Category: [Dashboard Widgets](https://pixelpapa.com/components/dashboard.md)
- Tags: dashboard, activity, feed, live, list
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/dashboard/dash-activity-feed

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/dash-activity-feed.json"
```

The file is written to `components/pixelpapa/dash-activity-feed.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { CreditCard, GitMerge, MessageSquare, Rocket, UserPlus, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";

export type ActivityKind = "deploy" | "merge" | "comment" | "payment" | "signup";
export type ActivityEvent = {
  id: string;
  actor: string;
  /** Verb phrase between the actor and the target, e.g. "deployed". */
  action: string;
  target: string;
  kind: ActivityKind;
  time: string;
};

const KINDS: Record<ActivityKind, { icon: LucideIcon; color: string }> = {
  deploy: { icon: Rocket, color: "var(--chart-1)" },
  merge: { icon: GitMerge, color: "var(--chart-3)" },
  comment: { icon: MessageSquare, color: "var(--chart-2)" },
  payment: { icon: CreditCard, color: "var(--chart-5)" },
  signup: { icon: UserPlus, color: "var(--chart-4)" },
};

const initials = (name: string) =>
  name
    .split(" ")
    .map((w) => w[0])
    .slice(0, 2)
    .join("");

type ActivityFeedProps = { events: ActivityEvent[]; className?: string };

/** Renders newest-first. Give each event a stable `id`; anything new animates in. */
export function ActivityFeed({ events, className }: ActivityFeedProps) {
  const reduce = useReducedMotion();
  return (
    <ol aria-live="polite" className={cn("relative flex flex-col", className)}>
      <AnimatePresence initial={false} mode="popLayout">
        {events.map((e) => {
          const { icon: Icon, color } = KINDS[e.kind];
          return (
            <motion.li
              key={e.id}
              layout={reduce ? false : "position"}
              initial={reduce ? { opacity: 0 } : { opacity: 0, y: -28, scale: 0.96 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, scale: 0.96, transition: { duration: 0.2 } }}
              transition={{ type: "spring", stiffness: 380, damping: 32 }}
              className="relative flex items-center gap-3 rounded-xl px-3 py-2.5"
            >
              {/* Arrival flash */}
              <motion.span
                aria-hidden
                className="absolute inset-0 rounded-xl bg-primary/10"
                initial={{ opacity: 1 }}
                animate={{ opacity: 0 }}
                transition={{ duration: 1.6, delay: 0.3 }}
              />
              <span className="relative shrink-0">
                <span
                  className="flex size-9 items-center justify-center rounded-full text-xs font-semibold"
                  style={{ background: `color-mix(in oklch, ${color} 16%, transparent)`, color }}
                >
                  {initials(e.actor)}
                </span>
                <span className="absolute -right-1 -bottom-1 flex size-4.5 items-center justify-center rounded-full border border-border bg-card text-muted-foreground">
                  <Icon className="size-2.5" />
                </span>
              </span>
              <p className="relative min-w-0 flex-1 truncate text-sm text-muted-foreground">
                <span className="font-medium text-card-foreground">{e.actor}</span> {e.action}{" "}
                <span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-card-foreground">{e.target}</span>
              </p>
              <time className="relative shrink-0 text-xs text-muted-foreground tabular-nums">{e.time}</time>
            </motion.li>
          );
        })}
      </AnimatePresence>
    </ol>
  );
}

const pool: Omit<ActivityEvent, "id" | "time">[] = [
  { actor: "Mara Lindqvist", action: "deployed", target: "api-gateway@4.12.0", kind: "deploy" },
  { actor: "Tobias Okafor", action: "merged", target: "#2184 retry-budget", kind: "merge" },
  { actor: "Northwind Labs", action: "paid invoice", target: "INV-00931 · $2,400", kind: "payment" },
  { actor: "Priya Raman", action: "commented on", target: "Q4 pricing draft", kind: "comment" },
  { actor: "Jonas Weller", action: "joined workspace", target: "design-systems", kind: "signup" },
  { actor: "Mara Lindqvist", action: "rolled back", target: "billing-worker@2.3.1", kind: "deploy" },
  { actor: "Ines Castell", action: "merged", target: "#2187 dark-tokens", kind: "merge" },
  { actor: "Harbor & Finch", action: "upgraded to", target: "Scale · 40 seats", kind: "payment" },
  { actor: "Dev Anand", action: "replied in", target: "incident-0412", kind: "comment" },
];
const VISIBLE = 5;
const INTERVAL = 2400;

export default function DashActivityFeedDemo() {
  const [tick, setTick] = useState(VISIBLE - 1);
  const [paused, setPaused] = useState(false);
  const pausedRef = useRef(paused);
  pausedRef.current = paused;

  useEffect(() => {
    const id = setInterval(() => {
      if (!pausedRef.current) setTick((t) => t + 1);
    }, INTERVAL);
    return () => clearInterval(id);
  }, []);

  // The newest event is `tick`; older ones are derived from it, so ages stay in sync.
  const events: ActivityEvent[] = Array.from({ length: VISIBLE }, (_, age) => {
    const n = tick - age;
    return { ...pool[n % pool.length], id: `e${n}`, time: age === 0 ? "now" : `${Math.round((age * INTERVAL) / 100) / 10}s` };
  });

  return (
    <div
      onPointerEnter={() => setPaused(true)}
      onPointerLeave={() => setPaused(false)}
      className="w-full max-w-md rounded-2xl border border-border bg-card p-2 shadow-sm"
    >
      <div className="flex items-center justify-between px-3 pt-2.5 pb-2">
        <p className="font-medium text-card-foreground">Activity</p>
        <span className="inline-flex items-center gap-2 text-xs text-muted-foreground">
          <span className="relative flex size-2">
            {!paused && <span className="absolute inset-0 animate-ping rounded-full bg-emerald-500/70 motion-reduce:hidden" />}
            <span className={cn("relative size-2 rounded-full transition-colors", paused ? "bg-amber-500" : "bg-emerald-500")} />
          </span>
          {paused ? "Paused" : "Live"}
        </span>
      </div>
      <div className="h-[280px] overflow-hidden">
        <ActivityFeed events={events} />
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Live Activity Feed" component from PixelPapa UI to this project.

What it is: A feed where new events drop in at the top on a timer with a brief highlight while older rows slide down and the last one fades out; pauses on hover.
Category: Dashboard Widgets. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/dash-activity-feed.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/dash-activity-feed.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/dash-activity-feed" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More dashboard widgets

- [Goal Progress Rings](https://pixelpapa.com/components/dashboard/dash-goal-progress.md): Concentric rings sweep toward each target beside a list of matching bars; hovering a goal isolates its ring and swaps the centre readout.
- [KPI Tiles](https://pixelpapa.com/components/dashboard/dash-kpi-tiles.md) (Pro): Metric tiles that count up to their value, show a coloured delta pill and draw an edge-to-edge sparkline along the bottom.
- [Live Visitors](https://pixelpapa.com/components/dashboard/dash-live-visitors.md) (Pro): A real-time visitors widget: digits flip as the count ticks, cities pulse on a dotted world map and a bar stream scrolls the last minute.
- [Revenue Card With Period Switcher](https://pixelpapa.com/components/dashboard/dash-revenue-card.md): A revenue card whose period tabs slide a pill, roll every digit of the total like an odometer and spring the bar chart to the new shape.
- [Team Table Mini](https://pixelpapa.com/components/dashboard/dash-team-table-mini.md) (Pro): A compact members table with presence dots, an inline role picker and a status filter whose pill slides while rows reflow into place.
- [Usage Meter](https://pixelpapa.com/components/dashboard/dash-usage-meter.md) (Pro): Two plan meters: a storage bar whose category segments fill in sequence, and a seat meter of spring-loaded pips you can add to or remove from.
