# Log Viewer

> A React log viewer for streaming logs up to 100,000 lines: level colors, level and regex filters, tail follow, ANSI colors and JSON details.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add log-viewer`
- Docs: https://wingo-ui.com/components/log-viewer

## Interaction

New lines stream in at the bottom and the view follows them. Scroll up (wheel, finger, scrollbar or keys) and following pauses by itself: a pill at the bottom counts the new lines (12 new lines) and a tap on it, the End key or scrolling back to the very end follows again; the follow toggle and the Live / Paused badge in the footer show the state. Type in the search to hide the lines that do not match (case and diacritics are ignored), or turn on the .* toggle for a regular expression (an invalid one is marked and filters nothing); in highlight mode every line stays, hits are marked and Enter / Shift+Enter or the arrows step through them. The level chips turn levels on and off and show their counts. Click, tap or press Enter on a line with details to open its JSON tree under it (the stream pauses so it stays put). The toolbar wraps lines, shows or hides the time, and the ⋯ menu copies the visible lines, downloads everything as a dated .log file or clears the buffer; on a narrow viewer wrap and time move into that menu, and below 768px it opens as a bottom sheet while the chips scroll sideways. The log body is one Tab stop: Up and Down move a focus row, Page Up / Page Down jump a page, Home goes to the first line, End to the newest, Escape drops the focus row (screen readers hear each focus row as it moves), and Ctrl+F / ⌘F inside the viewer jumps to its search (searchShortcut adds a global key). Lines over 10.000 characters are cut with a Show all button; with maxLines reached the oldest lines drop and the footer says how many.

## Installation

```bash
npx wingo-ui@latest add log-viewer
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/log-viewer` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/log-viewer.tsx`

npm dependencies: `npm i @tanstack/react-virtual motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Empty State](https://wingo-ui.com/components/empty-state) ([Markdown](https://wingo-ui.com/components/empty-state.md))
- [JSON Viewer](https://wingo-ui.com/components/json-viewer) ([Markdown](https://wingo-ui.com/components/json-viewer.md))
- [Search Input](https://wingo-ui.com/components/search-input) ([Markdown](https://wingo-ui.com/components/search-input.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Toggle](https://wingo-ui.com/components/toggle) ([Markdown](https://wingo-ui.com/components/toggle.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useClipboard](https://wingo-ui.com/components/use-clipboard) ([Markdown](https://wingo-ui.com/components/use-clipboard.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useStickToBottom](https://wingo-ui.com/components/use-stick-to-bottom) ([Markdown](https://wingo-ui.com/components/use-stick-to-bottom.md))
- [File export](https://wingo-ui.com/components/file-export) ([Markdown](https://wingo-ui.com/components/file-export.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useEffect, useState } from "react"
import { LogViewer, type LogLine, type LogViewerApi } from "@/components/ui/log-viewer"

const initial: LogLine[] = [
  { time: "2026-09-24T13:38:12.345Z", level: "info", source: "sync", message: "Fetching messages for tax ID 12-3456740" },
  { time: "2026-09-24T13:38:12.902Z", level: "warn", source: "sync", message: "Invoice 2026-0409: slow response, retrying (2/5)" },
  {
    time: "2026-09-24T13:38:13.410Z",
    level: "error",
    source: "validator",
    message: "Validation error: BR-CO-17 tax amount differs",
    meta: { invoice: "2026-0411", code: "BR-CO-17" },
  },
]

export function SyncJobLog({ events }: { events: EventSource }) {
  const [api, setApi] = useState<LogViewerApi | null>(null)

  useEffect(() => {
    if (!api) return
    const onMessage = (event: MessageEvent<string>) => api.append(event.data)
    events.addEventListener("message", onMessage)
    return () => events.removeEventListener("message", onMessage)
  }, [api, events])

  return (
    <LogViewer
      lines={initial}
      setApi={setApi}
      fileName="invoices-4812"
      searchShortcut="mod+shift+f"
      locale="en-US"
      labels={{ search: "Filter the log" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `lines?` | `LogLine[] \| string[]` | `"[]"` | The lines: { id?, time?, level?, message, source?, meta? } or raw strings (parsed with parseLogLine). Pass a new array to append: only the new tail is read; a shorter or different array starts over. For high rates use setApi().append, which never re-renders the parent. |
| `maxLines?` | `number` | `100000` | A ring buffer: past it the oldest lines drop and the footer says how many. Lowering it at runtime trims from the start. |
| `levels?` | `LogLevel[]` | `"[\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]"` | The level chips shown in the toolbar ("trace", "debug", "info", "warn", "error", "fatal"). |
| `levelFilter?` | `LogLevel[]` |  | Controlled visible levels. Lines without a level are always shown. |
| `defaultLevelFilter?` | `LogLevel[]` | `"[\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]"` | Visible levels on mount; every level by default. |
| `onLevelFilterChange?` | `(levels: LogLevel[]) => void` |  | A level chip was turned on or off. |
| `query?` | `string` |  | Controlled search text. |
| `defaultQuery?` | `string` | `""` | The search on mount. Words match anywhere in the message or source, ignoring case and diacritics; the filter runs 120ms after typing stops. |
| `onQueryChange?` | `(query: string) => void` |  | Every keystroke in the search. |
| `filterMode?` | `"filter" \| "highlight"` | `"filter"` | filter hides the lines that do not match; highlight keeps them, marks the hits and steps through them with Enter / Shift+Enter. |
| `regex?` | `boolean` | `false` | The search is a regular expression (case-insensitive). The .* toggle in the toolbar flips it; an invalid expression is marked and filters nothing. |
| `follow?` | `boolean` |  | Controlled following of the newest line. |
| `defaultFollow?` | `boolean` | `true` | Stick to the newest line on mount. Scrolling up pauses it by itself. |
| `onFollowChange?` | `(follow: boolean) => void` |  | Following paused (scroll up, a key, an opened line) or resumed (the pill, End, the toggle, scrolling to the end). |
| `wrap?` | `boolean \| "auto"` |  | Controlled line wrapping. |
| `defaultWrap?` | `boolean \| "auto"` | `"auto"` | auto wraps below 768px and keeps one line (scrolling sideways) above. |
| `onWrapChange?` | `(wrap: boolean) => void` |  | The wrap toggle was pressed. |
| `showTimestamps?` | `boolean` | `true` | The time column. The toolbar clock toggle flips it too. |
| `timeFormat?` | `"time" \| "datetime" \| "relative" \| "iso"` | `"time"` | time 16:38:12.345, datetime 24.09.2026, 16:38:12, relative 2 min ago (ticking), iso as stored. Local times print after hydration. |
| `showLevels?` | `boolean` | `true` | The 3-letter level tag (INF, WRN, ERR) in the level's tone. |
| `showSource?` | `boolean` | `true` | The source chip ([spv], [http]), truncated at 14 characters. |
| `showLineNumbers?` | `boolean` | `false` | Line numbers that keep counting after old lines drop. They are skipped when copying text. |
| `ansi?` | `boolean` | `true` | Renders ANSI colors (30 to 37, 90 to 97, backgrounds, bold, dim, italic, underline) with the tone palette; false strips them. |
| `expandable?` | `boolean` | `true` | Lines with meta open a JSON tree under them on click, tap or Enter. |
| `size?` | `"sm" \| "md"` | `"sm"` | 12px text on 24px rows or 13px on 28px rows. |
| `height?` | `number` | `420` | Height of the log body in px. |
| `toolbar?` | `boolean` | `true` | The row with search, level chips, toggles and the ⋯ menu. |
| `searchShortcut?` | `string` | `""` | A global key that focuses the search, like mod+shift+f (shown as a hint in the field). Empty binds nothing; Ctrl+F / ⌘F always works inside the viewer. |
| `fileName?` | `string` | `"logs"` | Download base name: logs-2026-09-24-1640.log. |
| `loading?` | `boolean` | `false` | Skeleton rows in place of the log; the toolbar waits. |
| `emptyState?` | `ReactNode` |  | Replaces the built-in No logs yet state. |
| `now?` | `Date \| number` |  | The clock for relative timestamps. Defaults to the current time, ticking every second. |
| `setApi?` | `(api: LogViewerApi) => void` |  | Called after mount with { append(lines), clear(), scrollToBottom(), follow(on), getLines() }. append takes a line, a string or arrays of either and batches them into one render per frame. |
| `locale?` | `string` | `""` | Counts and times. Empty uses the UIProvider locale, then the browser's. |
| `labels?` | `Partial<LogViewerLabels>` |  | Every string: log, search, regex, invalidRegex, levels { trace, debug, info, warn, error, fatal }, levelsGroup, wrap, timestamps, follow, live, paused, newLines(count, formatted), jumpToLatest, copy, download, clear, moreActions, actions, cancel, lines(…), errors(…), filtered(…), dropped(…), matches(index, total), previousMatch, nextMatch, empty, emptyDescription, noMatches, noMatchesDescription, clearFilters, showAll(…), details, loading, copied, copyFailed, downloaded(fileName). Pass only the keys you translate. |
| `classNames?` | `Partial<Record<"toolbar" \| "search" \| "chips" \| "chip" \| "actions" \| "body" \| "row" \| "number" \| "time" \| "level" \| "source" \| "message" \| "meta" \| "jump" \| "empty" \| "status", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the root panel, merged last. |

## Parts

### DEFAULT_LOG_VIEWER_LABELS (constant)

The English copy of the toolbar, filters, menus and announcements. Spread it into labels when you translate only a few keys.

### LogLine (type)

One parsed line: message, plus optional id, time, level, source and meta (shown as JSON details).

### parseLogLine (function)

Parses "2026-09-24T16:38:12.345Z INFO [spv] message", "[16:38:12] WARN message", "ERROR message" and JSON lines with level / msg / time (pino numeric levels too; the other keys become meta). Signature: (text: string) => LogLine.

### stripAnsi (function)

Removes ANSI escape sequences (colors, cursor moves, links). Signature: (text: string) => string.

### LOG_LEVELS (constant)

The level recipe (tone, 3-letter tag, row tint, text class), for legends and custom rows. Type: Record<LogLevel, { tone: Tone; tag: string; row: string; text: string }>.

## Examples

- **100,000 lines**: Virtualized: only the rows on screen are in the DOM. Scroll, filter and watch the frame counter.
- **Warnings and errors only**: The level chips filter with live counts; a controlled levelFilter.
- **Regex highlight**: Highlight mode keeps every line and marks BR-\w+-\d+ hits; Enter and Shift+Enter step through them.
- **Build with ANSI colors**: npm run build output: SGR colors mapped to the tone palette, line numbers, no wrap.
- **Parsed text lines**: Plain strings and JSON lines parsed by parseLogLine; new lines are appended from a new array.
- **On a phone**: Wrapped lines, a scrolling chip rail and the ⋯ menu that opens as a sheet below 768px.

## See also

- [JSON Viewer](https://wingo-ui.com/components/json-viewer) ([Markdown](https://wingo-ui.com/components/json-viewer.md))
- [useStickToBottom](https://wingo-ui.com/components/use-stick-to-bottom) ([Markdown](https://wingo-ui.com/components/use-stick-to-bottom.md))
- [Code Block](https://wingo-ui.com/components/code-block) ([Markdown](https://wingo-ui.com/components/code-block.md))
- [File export](https://wingo-ui.com/components/file-export) ([Markdown](https://wingo-ui.com/components/file-export.md))

---

Canonical: https://wingo-ui.com/components/log-viewer
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
