mdxui

ActivityPanel

Activity feed panel component for displaying event streams

ActivityPanel

The ActivityPanel component displays a chronological feed of user and system activities. It's commonly used to show recent actions, updates, notifications, and events in your application.

Type Signature

interface ActivityPanelProps {
  /** Panel title */
  title?: string
  /** Panel description */
  description?: string
  /** Activity items */
  activities: Array<{
    id: string
    type: string
    actor: string
    action: string
    target?: string
    timestamp: string
    metadata?: any
  }>
  /** Group activities by time */
  groupByTime?: boolean
  /** Show actor avatars */
  showAvatars?: boolean
  /** Activity click handler */
  onActivityClick?: (activity: Activity) => void
  /** Panel className */
  className?: string
  /** Collapsible panel */
  collapsible?: boolean
  /** Start collapsed */
  defaultCollapsed?: boolean
}

Usage

<ActivityPanel
  title="Recent Activity"
  activities={[
    {
      id: '1',
      type: 'comment',
      actor: 'John Doe',
      action: 'commented on',
      target: 'API Documentation',
      timestamp: '2024-01-15T10:30:00Z',
    },
    {
      id: '2',
      type: 'update',
      actor: 'Jane Smith',
      action: 'updated',
      target: 'User Settings',
      timestamp: '2024-01-15T09:15:00Z',
    },
    {
      id: '3',
      type: 'create',
      actor: 'System',
      action: 'created',
      target: 'New API Key',
      timestamp: '2024-01-15T08:00:00Z',
    },
  ]}
  groupByTime={true}
  showAvatars={true}
/>

Props

title

The panel title displayed in the header.

<ActivityPanel title="Recent Activity" activities={activities} />

description

Optional panel description or subtitle.

<ActivityPanel
  title="Team Activity"
  description="Latest updates from your team members"
  activities={activities}
/>

activities

Array of activity items to display. Each activity includes actor, action, target, and timestamp.

<ActivityPanel
  activities={[
    {
      id: '1',
      type: 'comment',
      actor: 'John Doe',
      action: 'commented on',
      target: 'Issue #123',
      timestamp: '2024-01-15T10:30:00Z',
      metadata: {
        avatar: 'https://...',
        comment: 'Great work on this!',
      },
    },
    {
      id: '2',
      type: 'update',
      actor: 'Jane Smith',
      action: 'updated',
      target: 'Project Settings',
      timestamp: '2024-01-15T09:15:00Z',
      metadata: {
        avatar: 'https://...',
        changes: ['name', 'description'],
      },
    },
  ]}
/>

groupByTime

Group activities by time periods (Today, Yesterday, This Week, etc.). Defaults to true.

<ActivityPanel
  groupByTime={true}
  activities={activities}
/>

showAvatars

Display actor avatars next to activities. Defaults to true.

<ActivityPanel
  showAvatars={true}
  activities={activities}
/>

onActivityClick

Callback when an activity item is clicked.

<ActivityPanel
  activities={activities}
  onActivityClick={(activity) => {
    console.log('Clicked activity:', activity)
    navigateToActivity(activity)
  }}
/>

Common Patterns

User Activity Feed

<ActivityPanel
  title="Your Activity"
  activities={[
    {
      id: '1',
      type: 'login',
      actor: 'You',
      action: 'logged in',
      timestamp: '2024-01-15T10:30:00Z',
    },
    {
      id: '2',
      type: 'update',
      actor: 'You',
      action: 'updated',
      target: 'Profile Settings',
      timestamp: '2024-01-15T09:00:00Z',
    },
    {
      id: '3',
      type: 'create',
      actor: 'You',
      action: 'created',
      target: 'New Project',
      timestamp: '2024-01-14T16:45:00Z',
    },
  ]}
/>

Team Activity Feed

<ActivityPanel
  title="Team Activity"
  showAvatars={true}
  activities={[
    {
      id: '1',
      type: 'commit',
      actor: 'Alice Johnson',
      action: 'pushed 3 commits to',
      target: 'main branch',
      timestamp: '2024-01-15T11:00:00Z',
      metadata: {
        avatar: 'https://...',
        commits: 3,
      },
    },
    {
      id: '2',
      type: 'review',
      actor: 'Bob Williams',
      action: 'reviewed',
      target: 'Pull Request #42',
      timestamp: '2024-01-15T10:30:00Z',
      metadata: {
        avatar: 'https://...',
        status: 'approved',
      },
    },
    {
      id: '3',
      type: 'issue',
      actor: 'Carol Davis',
      action: 'opened',
      target: 'Issue #256',
      timestamp: '2024-01-15T09:15:00Z',
      metadata: {
        avatar: 'https://...',
        priority: 'high',
      },
    },
  ]}
/>

System Events

<ActivityPanel
  title="System Events"
  activities={[
    {
      id: '1',
      type: 'deploy',
      actor: 'System',
      action: 'deployed',
      target: 'v2.1.0 to production',
      timestamp: '2024-01-15T12:00:00Z',
      metadata: {
        status: 'success',
        duration: '2m 34s',
      },
    },
    {
      id: '2',
      type: 'backup',
      actor: 'System',
      action: 'completed backup of',
      target: 'Database',
      timestamp: '2024-01-15T06:00:00Z',
      metadata: {
        size: '2.4 GB',
      },
    },
    {
      id: '3',
      type: 'alert',
      actor: 'System',
      action: 'sent alert:',
      target: 'High CPU usage detected',
      timestamp: '2024-01-15T03:30:00Z',
      metadata: {
        severity: 'warning',
      },
    },
  ]}
/>

API Request Log

<ActivityPanel
  title="Recent Requests"
  showAvatars={false}
  activities={[
    {
      id: '1',
      type: 'api_request',
      actor: 'API Key: pk_live_...',
      action: 'POST',
      target: '/api/v1/users',
      timestamp: '2024-01-15T10:30:45Z',
      metadata: {
        status: 200,
        duration: '45ms',
      },
    },
    {
      id: '2',
      type: 'api_request',
      actor: 'API Key: pk_live_...',
      action: 'GET',
      target: '/api/v1/users/123',
      timestamp: '2024-01-15T10:30:42Z',
      metadata: {
        status: 200,
        duration: '23ms',
      },
    },
  ]}
/>

Time Grouping

When groupByTime is enabled, activities are grouped into time periods:

Today

Today
─────────────────────────
• John commented on Issue #123
  2 hours ago

• Jane updated Project Settings
  3 hours ago

Yesterday

Yesterday
─────────────────────────
• Bob pushed 3 commits
  Yesterday at 4:30 PM

• Alice opened Pull Request #42
  Yesterday at 2:15 PM

This Week

This Week
─────────────────────────
• Carol created New API Key
  Monday at 9:00 AM

• System deployed v2.1.0
  Sunday at 6:00 PM

Earlier

Earlier
─────────────────────────
• Team backup completed
  Jan 10

• User logged in
  Jan 8

Activity Types

Different activity types can have custom styling and icons:

const activityTypes = {
  comment: { icon: 'MessageSquare', color: 'blue' },
  update: { icon: 'Edit', color: 'green' },
  create: { icon: 'Plus', color: 'purple' },
  delete: { icon: 'Trash', color: 'red' },
  login: { icon: 'LogIn', color: 'gray' },
  logout: { icon: 'LogOut', color: 'gray' },
  deploy: { icon: 'Rocket', color: 'green' },
  error: { icon: 'AlertCircle', color: 'red' },
  warning: { icon: 'AlertTriangle', color: 'yellow' },
}

<ActivityPanel
  activities={activities}
  renderActivity={(activity) => {
    const config = activityTypes[activity.type]
    return (
      <ActivityItem
        icon={config.icon}
        color={config.color}
        activity={activity}
      />
    )
  }}
/>

Panel Context Usage

ActivityPanel can be used in different contexts:

Dashboard Widget

Embedded in dashboard:

<DashboardView>
  <div className="grid grid-cols-3 gap-4">
    <div className="col-span-2">
      <MetricsPanel />
    </div>
    <div className="col-span-1">
      <ActivityPanel
        title="Recent Activity"
        activities={recentActivities.slice(0, 10)}
      />
    </div>
  </div>
</DashboardView>

Slideover Panel

Show full activity feed in slideover:

<Shell>
  <DashboardView>
    <Button onClick={() => setShowActivity(true)}>
      View Activity
    </Button>
  </DashboardView>

  <Slideover open={showActivity} onClose={() => setShowActivity(false)}>
    <ActivityPanel
      title="All Activity"
      activities={allActivities}
      groupByTime={true}
    />
  </Slideover>
</Shell>

Inline Panel

Side panel in layout:

<div className="flex h-screen">
  <div className="flex-1">
    <MainContent />
  </div>
  <div className="w-80 border-l">
    <ActivityPanel
      title="Activity Feed"
      activities={activities}
    />
  </div>
</div>

Activity details in modal:

<Dialog open={showDetails} onClose={() => setShowDetails(false)}>
  <ActivityPanel
    title="Activity Details"
    activities={selectedActivity ? [selectedActivity] : []}
  />
</Dialog>

Real-Time Updates

Implement real-time activity updates:

function RealtimeActivityPanel() {
  const [activities, setActivities] = useState([])

  useEffect(() => {
    // Subscribe to real-time updates
    const unsubscribe = subscribeToActivities((newActivity) => {
      setActivities((prev) => [newActivity, ...prev])
    })

    return () => unsubscribe()
  }, [])

  return (
    <ActivityPanel
      title="Live Activity"
      activities={activities}
      groupByTime={true}
    />
  )
}

Infinite Scroll

Load more activities on scroll:

function InfiniteActivityPanel() {
  const [activities, setActivities] = useState([])
  const [hasMore, setHasMore] = useState(true)

  const loadMore = async () => {
    const newActivities = await fetchActivities({
      offset: activities.length,
      limit: 20,
    })
    setActivities([...activities, ...newActivities])
    setHasMore(newActivities.length === 20)
  }

  return (
    <ActivityPanel
      title="Activity Feed"
      activities={activities}
      onScroll={(e) => {
        const { scrollTop, scrollHeight, clientHeight } = e.target
        if (scrollHeight - scrollTop === clientHeight && hasMore) {
          loadMore()
        }
      }}
    />
  )
}

Empty States

Handle cases with no activities:

<ActivityPanel
  title="Activity"
  activities={activities}
  emptyState={
    <div className="text-center py-8">
      <p className="text-muted-foreground">No recent activity</p>
      <p className="text-sm text-muted-foreground mt-2">
        Activity will appear here when users take actions
      </p>
    </div>
  }
/>

Filtering Activities

Allow filtering by activity type:

function FilterableActivityPanel() {
  const [filter, setFilter] = useState('all')
  const [activities, setActivities] = useState([])

  const filteredActivities = activities.filter(
    (a) => filter === 'all' || a.type === filter
  )

  return (
    <>
      <Select value={filter} onValueChange={setFilter}>
        <option value="all">All Activity</option>
        <option value="comment">Comments</option>
        <option value="update">Updates</option>
        <option value="create">Created</option>
        <option value="delete">Deleted</option>
      </Select>

      <ActivityPanel
        title="Activity Feed"
        activities={filteredActivities}
      />
    </>
  )
}

Accessibility

ActivityPanel implements accessibility best practices:

  • Semantic HTML - Uses <article>, <time> elements
  • ARIA labels - Descriptive labels for screen readers
  • Keyboard navigation - All interactive elements keyboard accessible
  • Time formatting - Human-readable relative times with ISO fallback
<aside role="complementary" aria-labelledby="activity-panel-title">
  <h2 id="activity-panel-title">Recent Activity</h2>
  <ul role="list">
    <li>
      <article>
        <p>
          <strong>John Doe</strong> commented on <a href="#">Issue #123</a>
        </p>
        <time datetime="2024-01-15T10:30:00Z">2 hours ago</time>
      </article>
    </li>
  </ul>
</aside>

Best Practices

Limit Initial Items

Show a reasonable number of activities initially:

// Good - show 10 most recent
<ActivityPanel
  activities={activities.slice(0, 10)}
/>

// Bad - showing too many can impact performance
<ActivityPanel
  activities={activities}  // 1000+ items
/>

Use Relative Time

Display timestamps as relative time for recent activities:

const formatTime = (timestamp) => {
  const now = new Date()
  const then = new Date(timestamp)
  const seconds = Math.floor((now - then) / 1000)

  if (seconds < 60) return 'just now'
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`
  return then.toLocaleDateString()
}

Combine similar activities to reduce noise:

// Instead of:
// • John liked Post #1
// • John liked Post #2
// • John liked Post #3

// Show:
// • John liked 3 posts

Provide Context

Include enough information to understand the activity:

// Good - clear context
{
  actor: 'John Doe',
  action: 'commented on',
  target: 'Pull Request #42: Fix login bug',
}

// Bad - unclear context
{
  actor: 'John',
  action: 'commented',
  target: '#42',
}

On this page