Skip to content
← Blocks

Activity Feed

A working activity feed with illustrative data and frontend interactions.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/activity-feed.json

Use it

Scroll horizontally for long lines
"use client";
import { ActivityFeed } from "@/components/jez-ui/blocks/activity-feed";

export default function Example() {
  return (
    <>
      <ActivityFeed />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import {
  ActivityFeed,
  ActivityFeedHeading,
  ActivityFeedEntries,
} from "@/components/jez-ui/blocks/activity-feed";

export default function Example() {
  return (
    <ActivityFeed>
      <ActivityFeedHeading />
      <ActivityFeedEntries />
    </ActivityFeed>
  );
}
Scroll horizontally for long lines
import { ActivityFeed, ActivityFeedHeader, ActivityFeedTitle, ActivityFeedItem, ActivityFeedHeading, ActivityFeedEntries } from "@/components/jez-ui/blocks/activity-feed";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type ActivityFeedOptions = {
  className?: string;
  items?: typeof ActivityFeedDefaultItems;
  heading?: React.ReactNode;
};

export type ActivityFeedProps = Omit<
  React.ComponentProps<"section">,
  keyof ActivityFeedOptions
> &
  ActivityFeedOptions;

ActivityFeedProps

Source

View 2 source files

registry/blocks/activity-feed.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { Check, MessageSquare, Paperclip } from "lucide-react";
import { cn } from "../ui/utils";
export type ActivityFeedOptions = {
  className?: string;
  items?: typeof ActivityFeedDefaultItems;
  heading?: React.ReactNode;
};
export type ActivityFeedProps = Omit<
  React.ComponentProps<"section">,
  keyof ActivityFeedOptions
> &
  ActivityFeedOptions;
const ActivityFeedDefaultItems = [
  {
    name: "Alex Morgan",
    initials: "AM",
    action: "shared a new homepage direction",
    time: "10 minutes ago",
    icon: Paperclip,
    detail: "Homepage exploration.fig · 2.4 MB",
  },
  {
    name: "Sam Patel",
    initials: "SP",
    action: "completed the accessibility review",
    time: "45 minutes ago",
    icon: Check,
    detail: "Keyboard navigation and focus states are ready for review.",
  },
  {
    name: "Robin Lee",
    initials: "RL",
    action: "left a note on the project brief",
    time: "2 hours ago",
    icon: MessageSquare,
    detail: "“The simpler direction gives the work much more room.”",
  },
];
function useActivityFeedModel({
  items = ActivityFeedDefaultItems,
  heading = "Activity",
  className,
  children,
  ...rootProps
}: ActivityFeedProps) {
  return { items, heading, className, children, rootProps };
}
const ActivityFeedCompositionContext = React.createContext<ReturnType<
  typeof useActivityFeedModel
> | null>(null);
function useActivityFeedComposition() {
  const context = React.useContext(ActivityFeedCompositionContext);
  if (!context)
    throw new Error("ActivityFeed parts must be inside ActivityFeed.");
  return context;
}
export function ActivityFeed(props: ActivityFeedProps) {
  const model = useActivityFeedModel(props);
  const { className, rootProps, children } = model;
  return (
    <ActivityFeedCompositionContext.Provider value={model}>
      <section {...rootProps} className={cn("", className)}>
        {children !== undefined ? (
          children
        ) : (
          <>
            <ActivityFeedHeading />
            <ActivityFeedEntries />
          </>
        )}
      </section>
    </ActivityFeedCompositionContext.Provider>
  );
}

export function ActivityFeedHeader({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="activity-feed-header"
      className={cn("mb-6 flex items-center justify-between", className)}
      {...props}
    />
  );
}
export function ActivityFeedTitle({
  className,
  ...props
}: React.ComponentProps<"h2">) {
  return (
    <h2
      data-slot="activity-feed-title"
      className={cn("text-lg font-semibold", className)}
      {...props}
    />
  );
}

export function ActivityFeedItem({
  className,
  ...props
}: React.ComponentProps<"li">) {
  return (
    <li
      data-slot="activity-feed-item"
      className={cn("relative flex gap-3 pb-7 last:pb-0", className)}
      {...props}
    />
  );
}

export function ActivityFeedHeading({
  children,
  ...props
}: Partial<React.ComponentProps<typeof ActivityFeedHeader>> & {
  children?: React.ReactNode;
}) {
  const { heading } = useActivityFeedComposition();
  return (
    <ActivityFeedHeader {...props}>
      {children === undefined ? (
        <>
          <ActivityFeedTitle>{heading}</ActivityFeedTitle>
          <span className="text-xs text-muted-foreground">Today</span>
        </>
      ) : (
        children
      )}
    </ActivityFeedHeader>
  );
}
export function ActivityFeedEntries({
  children,
  ...props
}: Partial<React.ComponentProps<"ol">> & { children?: React.ReactNode }) {
  const { items } = useActivityFeedComposition();
  return (
    <ol {...props}>
      {children === undefined
        ? items.map((item, i) => (
            <ActivityFeedItem key={item.name}>
              {i < items.length - 1 && (
                <span className="absolute bottom-0 left-4 top-9 w-px bg-border" />
              )}
              <span className="relative grid size-8 shrink-0 place-items-center rounded-full border border-border bg-muted text-xs font-medium">
                {item.initials}
              </span>
              <div className="min-w-0 flex-1 pt-1">
                <p className="text-sm leading-relaxed">
                  <strong className="font-medium">{item.name}</strong>{" "}
                  <span className="text-muted-foreground">{item.action}.</span>
                </p>
                <p className="mt-1 text-xs text-muted-foreground">
                  {item.time}
                </p>
                <div className="mt-3 flex items-start gap-2 rounded-lg border border-border/70 bg-muted/20 p-3 text-sm leading-relaxed">
                  <item.icon
                    className="mt-0.5 shrink-0 text-muted-foreground"
                    size={15}
                  />
                  {item.detail}
                </div>
              </div>
            </ActivityFeedItem>
          ))
        : children}
    </ol>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

lucide-react@0.577.0 · clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →