Day.js Relative Time Feed — Free HTML CSS JS Snippet

Day.js Relative Time Feed · Dashboards · Plain HTML, CSS & JS · Live preview

What's included

Features

Genuinely live relative labels
fromNow() recomputes fresh, does not cache a frozen string.
Periodic re-render loop
A 30-second interval keeps every timestamp accurate.
Simulated live insertions
New items arrive the same way a WebSocket push would.
Always-correct chronological order
Re-sorted by real time on every render, not just on insert.
Consistent "fresh" detection
Uses the same now-vs-then comparison as the label itself.
Deterministic seed data
Backdated sample items spread realistically, not all "just now".

About this UI Snippet

Day.js Relative Time Feed — Why "2 Minutes Ago" Needs to Keep Recomputing

Screenshot of the Day.js Relative Time Feed snippet rendered live

A relative timestamp like "2 minutes ago" is only accurate at the instant it's computed — leave it on screen without ever recalculating, and ten minutes later it's still lying. This snippet's core discipline is treating fromNow() as something to call again periodically, not a string to compute once and forget.

relativeTime is a plugin, not core Day.js

Day.js ships a deliberately minimal core and adds capabilities like fromNow() through plugins — dayjs.extend(dayjs_plugin_relativeTime) is what registers it, and skipping that line means .fromNow() simply doesn't exist on a dayjs object, a common first-time gotcha with the library.

fromNow() always compares against the real current moment

Calling item.time.fromNow() computes the difference between that stored timestamp and dayjs()'s value *at the moment fromNow() is called* — it does not cache or memoize a string from whenever the item was first created. That's what makes the periodic setInterval(render, 30000) meaningful: re-rendering doesn't just redraw the same labels, it recomputes each one fresh, so "2 minutes ago" genuinely becomes "3 minutes ago" without a page reload.

The feed re-sorts by time on every render, not just on insert

Rather than assuming new items always belong at the top (true for genuinely live pushes, but this snippet also seeds several backdated items on load), render() sorts a copy of the items array by timestamp descending every time it runs — which is what guarantees the visual order always matches actual chronological order regardless of the mix of seeded and live-pushed items.

"Fresh" gets its own visual treatment, computed the same way as the label

An item's green "fresh" styling is decided by dayjs().diff(item.time, 'minute') < 1 — the exact same kind of now-versus-then comparison fromNow() does internally, just checked directly for a boolean instead of formatted as a string. Both pieces of information are always in agreement because they're derived from the same live comparison at render time, not two different clocks.

Reusing it

Point the "new event arriving" setInterval at a real WebSocket message handler or Server-Sent Events listener instead of Math.random(), keep the same dayjs()-timestamped insert-and-render pattern, and this becomes a genuine live activity feed.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to discover the "frozen relative timestamp" bug the hard way. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why fromNow() must be called again periodically to stay accurate, rather than being computed once when an item is created, and why the plugin architecture requires calling dayjs.extend before fromNow() becomes available at all. The same assistant can help optimize it — ask whether re-rendering the entire list every 30 seconds is wasteful for a feed with many items, or whether only the timestamp text nodes should be updated in place instead of rebuilding the whole DOM list. It's also useful for extending the effect: ask it to connect the simulated live-event interval to a real WebSocket or Server-Sent Events endpoint, add a "mark all as read" feature, or group consecutive activity from the same person into one combined feed item. Treat the code less like a finished artifact and more like a starting point for a conversation.

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-updating activity feed with human-readable relative timestamps (like "2 minutes ago") using the Day.js library with its relative-time plugin (load Day.js's core script and its relativeTime plugin script from a CDN, no other library), in plain HTML, CSS, and JavaScript.

Requirements:
- Seed the feed with several sample activity items (a person's name, an action description, and a timestamp), spread across different points in the past several hours rather than all being very recent.
- Display each item with the person's initials as a colored avatar, a description combining their name and action, and a relative time label computed using the date library's relative-time formatting method (not a manually written "X minutes ago" string).
- Visually distinguish very recent items (for example, less than one minute old) with a distinct color or style, computed using the same kind of current-time comparison the relative label itself uses, so the two pieces of information can never disagree about how recent an item is.
- Periodically re-render the entire feed (for example every 30 seconds) so that every displayed relative time label stays accurate as real time passes, without requiring a page reload — verify that the underlying date library recomputes the relative time fresh on each call rather than caching a fixed string from when the item was created.
- Periodically simulate a new activity item arriving (for example every 10-15 seconds) with the current timestamp, inserting it into the feed and keeping the full list sorted by actual chronological time (most recent first) regardless of the mix of seeded and newly-arrived items.
- Cap the total number of visible items so the feed doesn't grow indefinitely as new items keep arriving.

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.

Source Code

<div class="rf-wrap">
  <div class="rf-card">
    <div class="rf-head">
      <div class="rf-title">Activity Feed</div>
      <span class="rf-live"><span class="rf-dot"></span>Live</span>
    </div>
    <ul class="rf-list" id="rfList"></ul>
  </div>
</div>

Step by step

How to Use

  1. 1
    Add the Day.js CDNLoad dayjs.min.js and the relativeTime plugin before the snippet's JS runs.
  2. 2
    Paste HTML, CSS, and JSA feed renders with six backdated items, newest first.
  3. 3
    Watch a fresh itemA newly-added item shows a green "just now" style label.
  4. 4
    Wait about 30 secondsRelative labels like "2 minutes ago" tick forward on their own.
  5. 5
    Wait about 12 secondsA new simulated event arrives at the top of the feed.
  6. 6
    Watch the feed reorderItems always stay sorted by real chronological time.

Real-world uses

Common Use Cases

Social and notification feeds
The standard "X commented 3 minutes ago" pattern.
Admin and ops activity logs
Recent-events views that stay accurate without reloading.
Live chat and messaging timestamps
Genuinely live relative time on every message.
Dashboard "recent activity" widgets
Pair with the live-updating realtime chart elsewhere in this collection for a fuller live dashboard.
Support ticket and comment threads
Accurate posted-time labels throughout a long session.
Learning Day.js plugins
A clear reference for relativeTime and the plugin system.

Got questions?

Frequently Asked Questions

Day.js deliberately ships a minimal core library and moves less universally-needed features like relative time formatting into optional plugins, which keeps the base library small. Calling dayjs.extend(dayjs_plugin_relativeTime) registers that plugin's functionality (including the fromNow() method) onto every dayjs object — without that line, .fromNow() is simply not a method that exists yet.

fromNow() calculates its output by comparing a stored timestamp against dayjs()'s value at the exact moment fromNow() is called — it does not store or cache a fixed string when an item is first created. A label rendered once and left alone would become inaccurate as real time passes, so this snippet re-runs render() (which recomputes every fromNow() call) every 30 seconds to keep every displayed label truthful.

While a genuinely live push would always be newer than everything already in the feed, this snippet also seeds several backdated sample items when it first loads, mixed at various times in the past. Sorting a copy of the full array by timestamp on every render guarantees correct chronological order regardless of that mix, rather than relying on an assumption (new items are always newest) that happens to be true only for the live-push case.

Both the fresh check and the fromNow() label are computed as a live comparison against dayjs() (the current moment) at render time — the fresh check specifically computes dayjs().diff(item.time, "minute") and checks if it's under 1. Because both values derive from the same now-versus-then comparison at the same render pass, the green "fresh" indicator and the text label can never disagree about how recent an item actually is.

Replace the setInterval that generates random events with a handler for your real data source — a WebSocket onmessage callback or an EventSource listener — that calls the same items.unshift({ ..., time: dayjs() }) and render() pattern whenever a real event arrives. The periodic label-refresh interval and the sorting logic require no changes regardless of where new items come from.