Skip to content

Activity Feed

The timeline activity feed displays a chronological list of events, actions, and transactions. It appears on dashboards and detail pages to provide context about what has happened.

Rina menyelesaikan transaksi #28491
+Rp 287.500
14:32
Budi memproses retur #R-0042
-Rp 54.000
14:15
Sistem menerima kiriman dari DC (48 item)
13:50
Dewi membuka shift pagi
08:00

Anatomy

Each feed item contains:

[Icon 32px]  Actor Name performed action          Rp +1.450.000
(colored     Description or context text           14:32
 circle)
ElementStyle
Icon container32px circle, colored background (soft variant), icon in center (16px)
Actor name14px, font-semibold, text-1
Action text14px, text-2, inline after actor
Amount (optional)font-mono, 13px, font-semibold. Green (#2D7D46) for positive, red (#C4463A) for negative
Timestampfont-mono, 12px, text-3, right-aligned
Connector line1px border vertical line connecting icons

Event Types

EventIconCircle ColorIcon Color
Sale / TransactionShoppingCart#E8F0EC#2D6B4A
Payment receivedCreditCard#E8F0EC#2D6B4A
RefundRotateCcw#FCEAE8#C4463A
Stock movementPackage#DBEAFE#2563EB
User actionUser#F3F1EC#57534E
ApprovalCheckCircle#E8F0EC#2D6B4A
RejectionXCircle#FCEAE8#C4463A
Alert / WarningAlertTriangle#FEF3C7#92400E
SystemSettings#F3F1EC#57534E

React Component

tsx
interface FeedItem {
  id: string
  icon: LucideIcon
  iconBg: string
  iconColor: string
  actor: string
  action: string
  amount?: { value: number; prefix?: string }
  timestamp: string
}

export function ActivityFeed({ items }: { items: FeedItem[] }) {
  return (
    <div className="relative">
      {items.map((item, i) => (
        <div key={item.id} className="flex gap-3 pb-4 relative">
          {/* Connector line */}
          {i < items.length - 1 && (
            <div className="absolute left-4 top-8 bottom-0 w-px bg-border" />
          )}

          {/* Icon */}
          <div
            className="w-8 h-8 rounded-full flex items-center justify-center shrink-0 relative z-10"
            style={{ backgroundColor: item.iconBg }}
          >
            <item.icon className="w-4 h-4" style={{ color: item.iconColor }} />
          </div>

          {/* Content */}
          <div className="flex-1 min-w-0 pt-0.5">
            <p className="text-sm">
              <span className="font-semibold text-1">{item.actor}</span>
              {' '}
              <span className="text-2">{item.action}</span>
            </p>
          </div>

          {/* Right side */}
          <div className="text-right shrink-0">
            {item.amount && (
              <p className={cn(
                'font-mono text-[13px] font-semibold',
                item.amount.value >= 0 ? 'text-success' : 'text-danger'
              )}>
                {item.amount.value >= 0 ? '+' : ''}
                {item.amount.prefix ?? 'Rp '}
                {Math.abs(item.amount.value).toLocaleString('id-ID')}
              </p>
            )}
            <p className="font-mono text-xs text-3">{item.timestamp}</p>
          </div>
        </div>
      ))}
    </div>
  )
}

Usage

AppContext
ho-financeStore activity feed on store detail page
store-adminDashboard recent activity
stock-app-webStock movement history

Grouping

When showing a full day's activity, group by date with a date separator:

--- Sabtu, 22 Maret 2026 ---
[feed items...]

--- Jumat, 21 Maret 2026 ---
[feed items...]

Date separator: text-3, 12px, font-mono, centered, with border lines on either side.

Loading & Empty States

  • Loading: Skeleton pulse on 3 placeholder rows
  • Empty: Isometric illustration + "Belum ada aktivitas" message

Don't

  • Don't show more than 20 items without pagination or "Muat lebih banyak" button
  • Don't mix different time zones --- always use the store's local time
  • Don't use the activity feed for real-time streaming --- poll every 30 seconds or use WebSocket with throttle

RetailOS - Sistem ERP Retail Modern untuk Indonesia