webdevpuneet.com

Live Viewer Count Badge — Free HTML CSS JS Live Stream Snippet

Live Viewer Count Badge · Dashboards · Plain HTML, CSS & JS · Live preview

CategoryDashboards

Copy & paste

Source Code

<div class="lvc-card">
  <div class="lvc-thumb" aria-hidden="true">
    <svg viewBox="0 0 240 135" width="100%" height="100%"><rect width="240" height="135" fill="#111827"/><circle cx="120" cy="60" r="34" fill="#374151"/><rect x="70" y="100" width="100" height="22" rx="6" fill="#374151"/></svg>
    <span class="lvc-live-badge"><span class="lvc-dot"></span>LIVE</span>
    <span class="lvc-viewers" id="lvcViewers"><span class="lvc-dot lvc-dot-small"></span><span id="lvcCount">1,284</span> watching</span>
  </div>
  <div class="lvc-meta">
    <p class="lvc-title">Building a UI component library from scratch</p>
    <p class="lvc-sub">DevStream · Started 42 minutes ago</p>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Constrained random-walk counter
Small frequent moves and occasional larger jumps simulate believable real audience movement.
Floor-clamped value
Math.max() prevents the count from ever drifting toward an implausible near-zero number.
Two visually distinct status dots
A pulsing red dot for "live," a static green dot for the viewer count — different meanings, different motion.
Scale-bump feedback on update
A brief CSS transform confirms each tick without being distracting.
Visibility-aware interval
Updates pause automatically when the browser tab is backgrounded, resuming when it's visible again.
Locale-formatted count
toLocaleString() adds thousands separators automatically for any locale.
Glass-morphism viewer pill
backdrop-filter: blur() keeps the counter legible over any thumbnail image.
Zero dependencies
Pure HTML, CSS, and vanilla JavaScript — no WebSocket library required for the demo.

About this UI Snippet

Live Viewer Count Badge — Random-Walk Counter with a Visibility-Aware Timer

Screenshot of the Live Viewer Count Badge snippet rendered live

A live viewer count only feels "live" if the number actually moves — a static count next to a pulsing red dot reads as obviously fake within a few seconds. This snippet drives a believable, gently fluctuating counter with a constrained random walk instead of either a static number or fully random noise.

A random walk, not random noise

Each tick() picks a direction (up 55% of the time, down 45%, so the count trends slightly upward like a growing stream) and a magnitude — usually a small move of 1 to 4 viewers, but 15% of the time a larger jump of 10 to 31, simulating a batch of viewers joining or leaving together (e.g. after a clip gets shared). current = Math.max(940, current + direction * magnitude) clamps the result so the count never drifts below a believable floor, preventing an unlucky streak of "down" ticks from crashing the number toward zero.

Two different dots for two different meanings

The "LIVE" badge's dot uses @keyframes lvcPulse fading between full and low opacity — a heartbeat signaling the stream is active. The viewer-count dot is deliberately static (green, no animation) — it represents current status, not a pulse — so the two indicators aren't visually competing for the same "this is animating, pay attention" attention.

A tiny scale bump on every update

Each tick briefly adds .lvc-bump (a 6% CSS transform: scale()) to the whole viewer pill and removes it 150ms later via clearTimeout/setTimeout, giving a subtle tactile confirmation that the number just changed — small enough not to be distracting on a 2.2-second cycle, noticeable enough to register subconsciously.

Pausing work in a backgrounded tab

The visibilitychange listener clears the interval when document.hidden is true and restarts it when the tab becomes visible again. A live counter nobody is looking at doesn't need to keep computing and repainting every 2.2 seconds — this is a small but genuine performance courtesy, especially if a page has several such widgets running simultaneously.

Customizing it

Replace the random-walk simulation with a real WebSocket or Server-Sent Events feed from your streaming backend, keeping the same .lvc-bump visual feedback and formatCount() thousands-separator formatting for whatever real number arrives.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA stream thumbnail renders with a pulsing "LIVE" badge and a viewer count that updates every 2.2 seconds.
  2. 2
    Watch the counterThe number drifts up and down in small steps, occasionally jumping by a larger amount, and briefly scales up on each change.
  3. 3
    Switch to another browser tabUpdates pause automatically; switching back resumes the counter.
  4. 4
    Replace the simulation with real dataIn the JS, swap the tick() function's random-walk logic for a value received from a WebSocket or polling endpoint.
  5. 5
    Adjust the update frequencyChange the 2200ms interval in setInterval(tick, 2200) to update more or less often.
  6. 6
    Tune the movement feelEdit the direction probability and magnitude ranges in tick() to make the count feel calmer or busier.

Real-world uses

Common Use Cases

Live streaming overlays
Show a believable, gently moving viewer count beside a pulsing red LIVE dot, using small frequent moves with occasional bigger jumps.
Webinar and virtual event pages
Display live attendee numbers that never read as fake, with Math.max() flooring the count so it cannot drift toward an implausible zero.
Flash sale and drop pages
Add a people viewing this now indicator that creates honest urgency, with a brief scale bump confirming each tick without distracting.
Live blogs and news pages
Show concurrent reader counts on breaking stories, pausing updates when the tab is backgrounded so a returning visitor does not see a huge jump.
Visibility-aware timer reference
Learn how to suspend a random-walk timer while hidden and resume it cleanly, a pattern useful for any simulated live number.
Related: Before/After Image Slider — CSS Only Radio Steps (No JavaScript)
See the Before/After Image Slider — CSS Only Radio Steps (No JavaScript) for a related misc pattern worth pairing with this one.
Related: Share-URL State Sync — Filters Encoded into a Copyable Link
See the Share-URL State Sync — Filters Encoded into a Copyable Link for a related misc pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Fully random numbers would make the count jump unpredictably between ticks with no continuity, which reads as obviously fake. A random walk changes the existing value by a small delta each time, so the number drifts naturally the way a real audience count would — mostly small moves, occasionally a bigger jump when a group joins or leaves together.

The tick() function rolls an 85/15 split: 85% of the time it applies a small 1-4 viewer move, and 15% of the time a larger 10-31 viewer swing, simulating something like a clip going semi-viral or a batch of viewers navigating away together. Without the occasional larger jump, the counter would feel too metronomic and uniform.

The Page Visibility API's visibilitychange event and document.hidden property let the script detect when the tab is backgrounded and clear the interval, avoiding unnecessary timer callbacks, DOM updates, and repaints for a widget nobody is currently looking at — then restart the interval once the tab becomes visible again.

Replace the body of tick() with logic that reads the latest count from a WebSocket message handler or a periodic fetch() poll, instead of computing a random delta. Keep calling countEl.textContent = formatCount(newValue) and toggling the .lvc-bump class so the same visual feedback applies to real updates.

Math.max(940, current + direction * magnitude) clamps every update to a floor value. Without this, a long unlucky streak of "down" ticks in the random walk could theoretically drift the number toward zero or negative, which would look obviously wrong for what's meant to represent an active live stream's audience.