Live Log Viewer — Free HTML CSS JS Snippet

Live Log Viewer · Dashboards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Auto-follow derived from scroll position with a 40px stick threshold — no follow checkbox to manage
"New logs" pill surfaced only while detached, instead of yanking the viewport mid-read
Ring buffer capped at 400 lines so a long-running stream keeps memory and DOM size flat
Single string build and one innerHTML write per render rather than per-line node appends
HTML escaped before search highlighting is injected, so log content can never inject nodes
Level toggles and text search composed in one filter, with an "N of M lines" footer
Level chips coloured to match their lines, doubling as the legend
Irregular arrival intervals so the stream reads like a real service rather than a metronome
Pause, resume and clear controls with a pulsing live-status indicator

About this UI Snippet

Live Log Viewer — Scroll-Anchored Autoscroll, Level Filtering & a Bounded Ring Buffer

Screenshot of the Live Log Viewer snippet rendered live

A log viewer is deceptively hard to get right, and the difficulty is never the rendering. It is that the component has to guess whether the reader wants to follow the stream or read something further up — and every implementation that gets this wrong becomes unusable at exactly the moment it matters, when something is going wrong and lines are arriving fast.

Auto-follow derived from scroll position, not a toggle

The rule here is the one every good terminal uses: if the viewport is within 40 pixels of the bottom, the viewer is following and new lines scroll into view. Scroll up by more than that and following stops immediately, so the line you are reading stays put no matter how much arrives underneath. Scroll back to the bottom and following resumes on its own. There is no checkbox, because a checkbox makes the reader manage state that their scroll position already expresses unambiguously. When new lines arrive while detached, a "New logs" pill appears rather than yanking the view.

A ring buffer so memory stays flat

Streams do not end, so an append-only viewer grows without limit until the tab dies. logs is capped at 400 entries and shift() drops the oldest as new ones arrive, which bounds both the array and the DOM. This is the single most important line in a long-lived log component, and the one most often missing from hand-rolled versions that were only ever tested for a minute.

One string build per frame, not one node append per line

Rendering rebuilds the visible list as a single HTML string and assigns it once. Appending elements individually forces layout work per line and gets visibly slow when a burst arrives or a filter changes. Building a string and writing it once is both simpler and dramatically faster at this scale, and it makes filtering — which changes which lines exist rather than adding to them — the same code path as streaming.

Escaping before highlighting, in that order

Search matches are wrapped in <mark>, which means the renderer writes HTML. The escape runs first, over the raw message, and the <mark> tags are inserted into the already-escaped string afterwards. Doing it the other way round — highlighting then escaping — would escape your own markup, and skipping the escape entirely would let any log line containing angle brackets inject nodes into the page. Log messages routinely contain user-supplied data, so this ordering is a security property, not a formatting detail.

Filters that compose

Level toggles and the text search are applied together in one visible() filter, and the footer reports "N of M lines" so it is always clear that a quiet console means an active filter rather than a dead stream. Level chips carry their own colour when active, so the filter row doubles as the legend for the lines below it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet into an AI assistant like Claude and ask it to replace the simulated timer with a real EventSource connection, including reconnect-with-backoff and a visible connection state on the status dot — that turns the demo into something you could actually point at a service. Other natural extensions: add windowed rendering so the buffer can hold tens of thousands of lines while only the visible slice is in the DOM; add structured-log support that parses JSON messages and lets you filter on a field rather than a substring; add a "copy visible lines" action that respects the current filters; or add timestamp-range selection so a user can pin the view to the minute an incident started.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a live streaming log viewer in plain HTML, CSS, and JavaScript — no frameworks or libraries.

Requirements:
- A fixed-height scrollable console that receives new log lines on an irregular timer, each with a level (debug/info/warn/error), a timestamp and a message.
- Implement auto-follow derived from scroll position, NOT a checkbox: measure scrollHeight - scrollTop - clientHeight on scroll and treat anything within about 40px of the bottom as "following". While following, new lines scroll into view; once the user scrolls up, the viewport must not move even as lines arrive.
- While detached, show a floating "New logs" pill that re-attaches and jumps to the bottom when clicked. Scrolling back to the bottom must re-attach automatically and hide the pill.
- Cap the log buffer with a ring buffer (shift the oldest past a maximum such as 400 lines) so memory and DOM size stay flat for a stream that never ends.
- Render by building one HTML string for the visible lines and assigning it once, rather than appending nodes per line — so streaming and filtering use the same code path.
- Provide four independent level toggle chips, coloured to match the lines they control so the filter row doubles as a legend, plus a text search that filters messages and highlights the matched substring with <mark>.
- IMPORTANT: HTML-escape the message FIRST, then insert the <mark> tags into the escaped string. Never highlight before escaping, and never skip escaping — log messages routinely contain user-supplied data.
- Show a footer reading "N of M lines" so a filtered view is never mistaken for a stalled stream, plus Pause/Resume and Clear controls and a pulsing live-status dot that greys out when paused.
- Style it as a dark terminal panel with monospace lines, coloured level labels, and a coloured left border plus tinted background on warn and error rows.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Watch the streamLines arrive at irregular intervals with a level, timestamp and message, and the pulsing green dot in the header indicates a live connection. Warnings and errors carry a coloured left border so they are findable while scrolling.
  2. 2
    Scroll up to detachScrolling more than 40px from the bottom stops auto-follow instantly, so the line you are reading stays under your cursor while new lines continue to accumulate below.
  3. 3
    Use the New logs pill to catch upWhile detached, arriving lines surface a pill at the bottom of the console. Clicking it re-attaches and jumps to the newest line; scrolling back down manually does the same thing.
  4. 4
    Filter by levelThe four level chips toggle independently and are coloured to match the lines they control, so the filter row is also the legend. Turning off debug and info is the fastest way to see only what went wrong.
  5. 5
    Search within messagesTyping filters to matching lines and highlights the matched substring inside each one. The footer shows "N of M lines" so a filtered view is never mistaken for a stalled stream.
  6. 6
    Pause or clearPause stops the stream and greys the status dot while keeping everything already received. Clear empties the buffer and re-attaches to the bottom.

Real-world uses

Common Use Cases

Deployment and build log output
The natural home for this component — CI output, deploy logs and job runners all stream and all need the follow/detach behaviour. Pair it with a status page so the log sits next to the service health it explains.
Live tail views in an observability tool
Level filters and substring search cover the great majority of what people do with a tail before reaching for a query language, and the bounded buffer means the view can be left open all day without degrading.
Reference for scroll-anchored streaming UIs
The same follow-versus-detach problem appears in chat transcripts, comment threads and AI response streams. The threshold-based approach here transfers directly and is the reason well-behaved chat apps do not scroll away from you mid-read.
Developer tooling and local dashboards
Debug consoles inside admin panels, webhook inspectors and background-worker monitors all need this shape, and none of them justify a dependency for it.
Teaching safe dynamic HTML
The escape-then-highlight ordering is a compact, concrete example of why the sequence of those two operations is a security decision rather than a stylistic one.
Support and diagnostics panels shown to end users
When customers need to see what a long-running import or sync is doing, a filtered, pausable log is far more reassuring than a spinner, and the level filter lets you hide debug noise by default.

Got questions?

Frequently Asked Questions

It measures the distance from the bottom on every scroll event — scrollHeight minus scrollTop minus clientHeight — and treats anything within 40 pixels as "following". That single derived boolean drives everything: new lines only scroll into view while it is true, and the New logs pill only appears while it is false. Scrolling back to the bottom re-attaches automatically.

Because a stream has no end. Without a cap, both the array and the DOM grow until the tab becomes unresponsive — a bug that never shows up in a short test and always shows up in production. shift() drops the oldest line as each new one arrives, keeping memory and render cost flat regardless of how long the viewer is left open. Raise MAX_LINES if you need more scrollback.

Yes, because of the ordering. The message is HTML-escaped first, and the <mark> tags are inserted into the already-escaped string afterwards. If you highlighted first you would escape your own markup; if you skipped escaping, a log line containing a script tag would execute. Log messages frequently contain user input, so this ordering matters.

Replace the push() timer with your transport and call push() with real records. For Server-Sent Events, new EventSource(url).onmessage pushes each parsed payload; for WebSockets, do the same in onmessage. Nothing else changes — the buffer, filtering, follow behaviour and rendering are all independent of where the lines came from.

Because filtering and streaming then share one code path. Appending is only cheaper when nothing is filtered, and it forces a full rebuild anyway the moment a level toggle or search term changes. At a few hundred lines, one string build plus one innerHTML write is fast enough that the simpler model wins; for tens of thousands of lines you would move to windowed rendering.

Yes. Keep the log array in a ref rather than state and batch renders, since a high-frequency stream calling setState per line will re-render far more often than the screen repaints. Keep the derived following boolean in a ref too, read scroll geometry from a ref to the console element, and perform the scroll-to-bottom in a layout effect after the new lines have been committed.