Audit Log React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React audit log table: who did what, when and to which record, with filters, day groups, before / after diffs and a CSV export.
Search the log (actor, sentence, record and IP match, accents ignored), pick a period with quick ranges (Today, Last 7 days, Last 30 days, This month) and add filter chips for user, action, record type, severity or IP; the count updates and is announced. On desktop each event is a table row under a sticky day heading: click the row or its chevron button (Enter / Space) to open the before / after diff and the technical details, where IP, request id and metadata copy on click. The record in the sentence is a link. Tab reaches the toggles, links and copy buttons in order. Export CSV downloads exactly the events on screen (disabled when there are none). Below 768px the table becomes a feed of day groups: tap a row to expand it inline, the record link moves into the details, the period and filters open as bottom sheets, and Load more is a full-width button (or loads on scroll with autoLoadMore). Rows added by Load more slide in; the first page never animates. With reduced motion the rows and details appear without movement.
Audit Log is a Pro React component in the React table and list components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add audit-log, together with Avatar, Badge, Button, Copy Button, Date Range Picker and 18 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add audit-logAudit Log is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import AuditLog from @/components/ui/audit-log.
import { FilePlus2, FileX2, Pencil } from "lucide-react"
import { AuditLog, type AuditActionDef, type AuditEvent } from "@/components/ui/audit-log"
const actions: Record<string, AuditActionDef> = {
"invoice.issued": { label: "issued", icon: <FilePlus2 />, tone: "info", group: "Invoices" },
"invoice.cancelled": { label: "voided", icon: <FileX2 />, tone: "warning", group: "Invoices" },
"client.updated": { label: "updated", icon: <Pencil />, group: "Clients" },
}
const events: AuditEvent[] = [
{
id: "e-1",
at: "2026-09-24T16:35:00",
actor: { id: "jessica", name: "Jessica Dunn", avatar: "/avatars/jessica.jpg" },
action: "invoice.cancelled",
target: { type: "invoice", id: "2026-0398", label: "Invoice #2026-0398", href: "/invoices/2026-0398" },
severity: "warning",
changes: [{ field: "status", label: "Status", type: "status", from: { label: "Issued", tone: "info" }, to: { label: "Voided", tone: "danger" } }],
ip: "93.114.52.211",
device: "Firefox · Windows",
},
{
id: "e-2",
at: "2026-09-24T16:12:00",
actor: { id: "anna", name: "Anna Price" },
action: "client.updated",
target: { type: "client", id: "c04", label: "Greenleaf Pharmacy" },
changes: [{ field: "balance", label: "Balance", type: "money", from: 7315.2, to: 0 }],
},
]
export function ActivityLog() {
return (
<AuditLog
events={events}
actions={actions}
targetTypes={{ invoice: "Invoice", client: "Client" }}
currency="USD"
exportFileName="activity-log"
labels={{ search: "Search activity", export: "Download CSV" }}
/>
)
}Props
events*AuditEvent[]The events, newest first ({ id, at, actor: { id, name, avatar?, kind? }, action, target?: { type, id, label, href? }, summary?, changes?, severity?, ip?, device?, location?, requestId?, metadata? }). Not sorted unless sortOrder says so.
actionsRecord<string, AuditActionDef>Labels, icons, tones and groups per action key: { "invoice.cancelled": { label: "voided", icon, tone: "warning", group: "Invoices" } }. An unknown key prints the key.
targetTypesRecord<string, string>Record type names for the filter and the export: { invoice: "Invoice" }.
modedefault"client""client" | "server"client filters, searches and limits the period here. server leaves the events alone and only calls onQueryChange; pass the result and totalCount.
onQueryChange(query: { search: string; period: DateRange | null; filter: FilterGroup }) => voidCalled 300ms after the search, the period or the filter last changed (never on mount).
defaultSearchdefault""stringThe search the log starts with. Use search / onSearchChange to control it.
searchstringThe search, controlled.
onSearchChange(search: string) => voidEvery keystroke of the search.
filterFilterGroupThe filter, controlled (a lib/filters group).
defaultFilterFilterGroupThe filter the log starts with; an empty AND group by default.
onFilterChange(filter: FilterGroup) => voidEvery change of the filter chips.
filterFieldsFilterBuilderField<AuditEvent>[]The filterable fields. Default defaultAuditFilterFields(actions, the events' actors): user, action, record type, severity, IP.
viewsFilterView[]Saved views in the builder's menu ("Deletions only", "Critical actions").
periodDateRange | nullThe period, controlled.
defaultPeriodDateRange | nullThe period the log starts with; null shows everything.
onPeriodChange(period: DateRange | null) => voidA picked range, or null when cleared.
groupByDaydefaulttruebooleanDay headings ("Today", "Yesterday", "Monday, September 21") that stick under the table header. Events out of order land under their own day.
sortOrderdefault"none""desc" | "asc" | "none"none keeps the order you pass; desc and asc sort by time.
expandabledefaulttruebooleanRows with changes or details expand into the before / after diff and the IP, device, location and request id.
expandedKeysstring[]The expanded event ids, controlled.
defaultExpandedKeysstring[]Event ids expanded at first.
onExpandedKeysChange(keys: string[]) => voidA row was opened or closed.
timeFormatdefault"relative""relative" | "time" | "datetime"relative: "5 min ago" today and the time under older day headings. time: the clock (with the date when not grouped). datetime: date and time.
showIpdefaulttruebooleanThe IP column on desktop. The IP stays in the details either way.
showSeveritydefaulttruebooleanThe severity column: notice, warning and critical as soft badges with an icon; info stays quiet.
searchabledefaulttruebooleanThe search field at the start of the toolbar.
filterabledefaulttruebooleanThe filter builder: chips on desktop, a Filters button with a bottom sheet on phones.
showPerioddefaulttruebooleanThe period picker with quick ranges.
exportabledefaulttruebooleanThe export button: onExport, or a CSV download of the events on screen. Disabled with no events.
onExport(events: AuditEvent[], query: AuditLogQuery) => (void | Promise<unknown>)Replaces the built-in CSV (a server export). A returned promise shows the button loading; a rejection is announced.
exportFileNamedefault"audit-log"stringThe CSV name before the date: "audit-log-2026-09-24.csv".
csvDelimiterdefault",""," | ";"The CSV separator. ";" opens straight in Excel set to a European locale, which reads "," as the decimal mark.
onEventClick(event: AuditEvent) => voidThe record link's click when the target has no href.
renderSummary(event: AuditEvent, parts: { actor: ReactNode; verb: ReactNode; target: ReactNode }) => ReactNodeA custom sentence from the three parts. event.summary replaces the sentence for one event.
densitydefault"default""compact" | "default"Row height: 48px with a second line for times and actor kinds, or 36px on one line.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corners of the search, the period, the export and load-more buttons.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The active filter chips and the period's selection.
loadingdefaultfalsebooleanSkeleton rows with the real shape (feed rows on phones) in place of the events.
skeletonCountdefault8numberHow many skeleton rows loading shows (phones show at most 6).
refreshingdefaultfalsebooleanKeeps the rows and shows a thin indeterminate bar at the top while a request runs.
errordefault""ReactNodeReplaces the rows with an error state and a Try again button (onRetry). Text or a node is the message; true shows labels.error.
onRetry() => (void | Promise<unknown>)The error state's Try again; a promise shows it loading.
emptyStateReactNodeReplaces the empty state when there are no events at all.
hasMoredefaultfalsebooleanShows Load more under the rows. Big logs page on the server with it.
onLoadMore() => (void | Promise<unknown>)Loads the next page; a returned promise keeps the button loading.
loadingMoredefaultfalsebooleanShows Load more in its loading state (for a request started elsewhere).
autoLoadMoredefaultfalsebooleanLoads the next page when the Load more button scrolls within 200px of view.
maxRowsdefault200numberRenders at most this many events (0 renders all) and says so under the list. Beyond a few hundred, page on the server with hasMore.
totalCountnumberThe count in the toolbar when the server knows the total (server mode).
currencydefault"USD"stringISO 4217 code for money changes without their own currency.
nowDate | numberThe reference time for relative times and day headings. Default: the current minute (useNow).
localedefault""stringDates, times, numbers and money. Empty uses the UIProvider locale, then the browser's.
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for record links (default the navigation provider's, else <a>).
labelsPartial<AuditLogLabels>Every string, English by default (DEFAULT_AUDIT_LOG_LABELS): search, period, export, count(n, formatted), column names, severities, expand / collapse, emptyValue, fieldCount, rangePresets, csvHeaders and more.
filterBuilderPropsPartial<FilterBuilderProps<AuditEvent>>Extra props for the filter builder: its labels, applyMode, onSaveView, defaultActiveViewId.
dateRangePickerPropsPartial<DateRangePickerProps>Extra props for the period picker: its labels, min, numberOfMonths, your own presets.
classNamesPartial<Record<"toolbar" | "search" | "period" | "filters" | "export" | "count" | "refreshing" | "table" | "group" | "row" | "details" | "diff" | "meta" | "feed" | "item" | "more" | "empty" | "error", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root. Set --table-sticky-top here to sit the sticky header under an app bar.
Parts
AuditDiff
componentThe before / after table alone: values formatted by type (money, number, date, boolean, status badge, list), empty values muted. auto stacks the rows below 448px of room. Props: { changes: AuditChange[]; currency?: string; locale?: string; layout?: "auto" | "table" | "stack"; labels?; classNames? }.
defaultAuditFilterFields
functionThe default filter fields: user, action, record type, severity and IP, for your own builder or a server that filters with lib/filters. Signature: (actions, actors, options?: { labels?; targetTypes?; types? }) => FilterBuilderField<AuditEvent>[].
auditEventsToCsv
functionThe export's rows (header first) for toCsv / downloadCsv from lib/spreadsheet. Signature: (events: AuditEvent[], options?: { actions?; labels?; targetTypes?; locale?; currency?; header? }) => (string | null)[][].
formatAuditValue
functionA change value as text, the way the diff prints it. Signature: (value: unknown, change: AuditChange, context: { locale; currency; labels }) => string.
visibleChanges
functionThe changes with a from or a to (the ones the diff shows). Signature: (changes?: AuditChange[]) => AuditChange[].
DEFAULT_AUDIT_LOG_LABELS
constantThe English strings, to spread under your translations. Type: AuditLogLabels.
Examples
Phone feed
At 390px the table becomes a day-grouped feed; tap a row to open its changes.
Server mode
Search, period and filters go to a fake endpoint with 600ms latency.
AuditDiff
The before / after table of a client edit, on its own.
Saved views
Built-in views for deletions, critical actions and integrations.
States
Loading skeletons, an error with retry and a search with no results.
Compact
Dense rows, full timestamps and no IP column.
Dependencies
Files
- components/ui/audit-log.tsx
FAQ
Is the React Audit Log component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Audit Log?
Run npx wingo-ui@latest add audit-log. It also adds the Wingo UI items it builds on (Avatar, Badge, Button, Copy Button, Date Range Picker and 18 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/audit-log.
How does Audit Log behave on phones?
Below 768px the table becomes a feed of day groups: tap a row to expand it inline, the record link moves into the details, the period and filters open as bottom sheets, and Load more is a full-width button (or loads on scroll with autoLoadMore).
See also
Used in
More React table and list components
Articles using this component
- Audit Log UI in React: Filters, Diffs and CSV Export
Component guides10 min read
- Role Management UI in React: Members, Roles, Audit Log
Build it: screens and apps9 min read
- React Diff Viewer in React 19: Split, Unified, Word Diffs
Component guides9 min read