Data Display

EventStreamViewer

Dense append-only log of timestamped events with follow-latest scrolling, severity tones, expandable JSON details, and copy actions.

import { EventStreamViewer } from '@lostgradient/cinder/event-stream-viewer';
logstreamevents
01

Overview

Dense append-only viewer for timestamped operational events. Use it for workflow run logs, job output streams, webhook traces, activity completion feeds, and other real-time or historical event consoles.

Overview

EventStreamViewer renders a scrollable list of timestamped events, each with a severity tone, optional source label, a one-line summary, and optional expandable JSON details. It handles follow-latest scrolling (auto-scroll to bottom as events arrive), a paused state when the user scrolls away, filtering hooks, copy actions, reconnect boundaries, and sequence-gap markers. Empty, loading, disconnected, and truncated states are built in.

Usage

svelte
<script lang="ts">
  import { EventStreamViewer } from '@lostgradient/cinder/event-stream-viewer';
  import type { StreamEvent } from '@lostgradient/cinder/event-stream-viewer';

  let events: StreamEvent[] = $state([]);
</script>

<EventStreamViewer {events} label="Workflow run events" connectionState="connected" />

With filtering

When you pass an onFilter callback, the viewer renders a search input. Your component is responsible for filtering the events array in response to the callback.

svelte
<script lang="ts">
  import { EventStreamViewer } from '@lostgradient/cinder/event-stream-viewer';
  import type { StreamEvent } from '@lostgradient/cinder/event-stream-viewer';

  let allEvents: StreamEvent[] = $state([]);
  let query = $state('');

  const filteredEvents = $derived(
    query
      ? allEvents.filter((e) => e.summary.toLowerCase().includes(query.toLowerCase()))
      : allEvents,
  );
</script>

<EventStreamViewer
  events={filteredEvents}
  filterQuery={query}
  onFilter={(q) => {
    query = q;
  }}
  label="Filtered event stream"
/>

With copy-visible action

svelte
<script lang="ts">
  import { EventStreamViewer } from '@lostgradient/cinder/event-stream-viewer';
  import type { StreamEvent } from '@lostgradient/cinder/event-stream-viewer';

  let events: StreamEvent[] = $state([]);

  function handleCopyVisible(text: string) {
    navigator.clipboard.writeText(text);
  }
</script>

<EventStreamViewer {events} onCopyVisible={handleCopyVisible} label="Event stream" />

Follow-latest with bindable state

The followLatest prop is bindable. Bind it to read whether the viewer is paused or to programmatically resume following.

svelte
<script lang="ts">
  import { EventStreamViewer } from '@lostgradient/cinder/event-stream-viewer';
  import type { StreamEvent } from '@lostgradient/cinder/event-stream-viewer';

  let events: StreamEvent[] = $state([]);
  let followLatest = $state(true);
</script>

<p>Status: {followLatest ? 'Following' : 'Paused'}</p>
<EventStreamViewer {events} bind:followLatest label="Event stream" />

Event structure

Each event in the events array can be a normal StreamEvent or an additive reconnect boundary:

ts
type StreamEvent = {
  id: string; // Stable unique identifier (required)
  sequence?: number; // Optional integer sequence for gap detection
  datetime: string; // ISO 8601 datetime (required, used for machine-readable time)
  timestamp?: string; // Human-readable label e.g. "14:32:01" (falls back to datetime)
  severity?: 'debug' | 'info' | 'success' | 'warning' | 'error';
  source?: string; // Origin label e.g. "worker-1", "payment-service"
  summary: string; // One-line event description (required)
  details?: unknown; // Optional JSON payload rendered in a collapsible JsonViewer
};

type StreamReconnectedBoundary = {
  id: string; // Stable unique identifier (required)
  kind: 'reconnected';
  datetime?: string; // Optional ISO 8601 reconnect time
  timestamp?: string; // Optional human-readable reconnect label
  replayedCount: number; // Number of events replayed after reconnecting
};

type EventStreamEntry = StreamEvent | StreamReconnectedBoundary;

Reconnect and gap markers

Pass EventStreamEntry[] when a retained stream can resume after a network reconnect. A { kind: 'reconnected' } entry renders a visible divider labeled Reconnected — N events replayed.

Set detectSequenceGaps only when events is the complete, unfiltered stream. When consecutive rendered StreamEvent entries both provide integer sequence values and the later value is not the previous value plus one, the viewer inserts a sequence-gap marker before the later event. The marker shows the expected and received sequence numbers, for example Sequence gap — expected 8, received 10.

The onCopyVisible callback includes reconnect and sequence-gap markers in the copied text, so the copied stream preserves the same replay and continuity context users saw on screen.

Accessibility

The viewer exposes a role="log" region, which carries implicit polite live-region semantics so screen readers announce newly appended events. The region is keyboard focusable (tabindex="0") to support arrow-key scrolling.

Each event's timestamp is wrapped in a <time datetime="..."> element with the machine-readable ISO value, so assistive technology can parse the precise moment. The visible label is separate from the machine-readable datetime attribute.

Severity tones are conveyed both visually (color bar, badge text) and through an aria-label on the badge element. Source labels and summary text are part of the document flow and are read naturally.

The JSON details panel is toggled by a button with aria-expanded and aria-controls. Copy actions have descriptive aria-label attributes.

Reconnect boundaries render as visible dividers with role="separator" and a matching accessible label. Sequence gaps render as visible notes with role="note" and an accessible label that includes the expected and received sequence numbers.

A visually-hidden live region (always in the DOM, never removed with {#if}) announces copy confirmation to screen readers without double-announcing on the interactive buttons.

See event-stream-viewer.a11y.md for the full accessibility contract.

Live preview
02

When to use

Use when
  • Displaying real-time or historical operational events such as workflow steps, job logs, or webhook traces.
  • Showing an append-only diagnostic stream with filtering, copy, and structured detail expansion.
Avoid when
  • Showing a social activity feed or notification timeline — use feed instead.
  • Rendering paginated historical records with sorting — use data-table instead.
03

Examples

Basic event stream

A minimal operational event log with severity tones, timestamps, and source labels. Suitable for job runners, deploy logs, and webhook traces.

Reconnect replay with sequence gap

Shows a reconnect boundary after retained events replay, followed by an advisory sequence-gap marker.

Loading and empty states

Demonstrates the loading skeleton, empty state, and truncated notice variants.

With search filtering

Consumer-controlled filtering via the onFilter callback. The viewer renders a search input; the consumer owns the filtered events array.

Workflow run with retries and signals

A realistic workflow run event stream with activity completions, retry attempts, inbound signals, and a structured failure trace with expandable JSON details.

04

Props

Props for event-stream-viewer
Name Type Default Required Bindable Description
events EventStreamEntry[] req Events and additive boundary entries to render in chronological order, oldest first.
connectionState 'connected' | 'connecting' | 'disconnected' | 'error' Current connection state. When provided, renders a StatusDot connection preset in the toolbar. Omit when the stream has no live transport.
followLatest boolean true bind When true, new events automatically scroll the list to the bottom. Set to false to pause follow-latest (e.g. while the user reads earlier events). Bindable so the parent can read the paused state the component sets internally.
truncated boolean false Whether to show the "events were truncated" notice. This is a boolean flag, not a count: the viewer never slices events itself. Set it to true when you have already trimmed the array (e.g. capped retention) and want users to know earlier events are not shown.
loading boolean false Show a loading skeleton instead of the event list. Use while the first batch of events is in flight.
label text 'Event stream' Accessible label for the event list region. Required for accessibility. Defaults to "Event stream".
detectSequenceGaps boolean false Enables sequence-gap markers when events is the complete, unfiltered stream. Leave false for retained windows, severity-filtered subsets, search results, or any other caller-trimmed event array.
onCopyVisible (text: string) => void Callback fired when the user clicks the "Copy visible" toolbar action. Receives the text of all currently visible events. When omitted the copy action is hidden.
onFilter (query: string) => void Callback fired when the user updates the filter query in the toolbar's search field. The consumer is responsible for filtering events in response. When omitted the filter input is hidden.
filterQuery text '' Current filter query value, for controlled usage. Pairs with onFilter.