Relative Time
stimeo--relative-time
Renders a timestamp as "3 minutes ago" and keeps it fresh, via Intl.
The stimeo--relative-time controller follows the HTML <time> semantics. It computes the difference from the datetime attribute to now and formats it with Intl.RelativeTimeFormat — a browser standard, no added dependency. The polling interval widens as the timestamp ages (seconds → minutes → hours → days), and past a threshold it falls back to the authored absolute text (data-state="absolute"). The machine-readable datetime is left untouched while only the visible text updates, and the element is intentionally not a live region, so updates never interrupt a screen reader. The polling timer is torn down on disconnect (Turbo included). Behavior only — styling is owned by this Playground.
- Posted
- Updated
- Due
- Archived
The text refreshes on its own as time passes (the interval widens as the stamp ages). Items within a few minutes update about once a minute, so if you keep this page open the first “Posted” will tick to “4 minutes ago”, “5 minutes ago”, and so on. Updates are silent, to avoid interrupting screen readers. Hover any time to see its absolute value.
<%# Markup for the relative-time demo.
Formats the absolute time in <time datetime> into "3 minutes ago" etc. via
Intl.RelativeTimeFormat, updating at an interval that grows with elapsed time. The
machine-readable datetime stays fixed and only the display text updates; it's not a
live region, to avoid interrupting screen readers. The locale follows the page
language. The last example, past the threshold, falls back to an absolute format. %>
<ul class="relative-time-demo">
<% [
{ at: 3.minutes.ago, label: t("components.relative_time.demo.posted") },
{ at: 2.hours.ago, label: t("components.relative_time.demo.updated") },
{ at: 3.days.from_now, label: t("components.relative_time.demo.due") }
].each do |row| %>
<li class="relative-time-demo__row">
<span class="relative-time-demo__label"><%= row[:label] %></span>
<time
class="relative-time"
data-controller="stimeo--relative-time"
datetime="<%= row[:at].iso8601 %>"
title="<%= row[:at].strftime("%Y-%m-%d %H:%M") %>"
data-stimeo--relative-time-locale-value="<%= I18n.locale %>">
<%= row[:at].strftime("%Y-%m-%d %H:%M") %>
</time>
</li>
<% end %>
<li class="relative-time-demo__row">
<span class="relative-time-demo__label"><%= t(
"components.relative_time.demo.archived"
) %></span>
<%# Example that falls back to an absolute format past the threshold
(30 days = 2592000 seconds). %>
<time
class="relative-time"
data-controller="stimeo--relative-time"
datetime="<%= 90.days.ago.iso8601 %>"
data-stimeo--relative-time-locale-value="<%= I18n.locale %>"
data-stimeo--relative-time-threshold-value="2592000">
<%= 90.days.ago.strftime("%Y-%m-%d %H:%M") %>
</time>
</li>
</ul>
<p class="relative-time-demo__hint"><%= t("components.relative_time.demo.hint") %></p>
/*
* Presentation-only styles for the relative-time demo.
* The library only updates the element's text and reflects data-state (relative / absolute).
*/
.relative-time-demo {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 32rem;
}
.relative-time-demo__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
}
.relative-time-demo__label {
color: var(--color-text-muted);
font-size: 0.9rem;
}
.relative-time {
font-variant-numeric: tabular-nums;
color: var(--fg);
}
/* Times that fell back to absolute format are shown subtly, in a more monospace style. */
.relative-time[data-state="absolute"] {
font-size: 0.9rem;
color: var(--color-text-muted);
}
/* Explanatory caption: the relative text refreshes automatically over time. */
.relative-time-demo__hint {
margin: 0.75rem 0 0;
max-width: 32rem;
font-size: 0.85rem;
line-height: 1.5;
color: var(--color-text-muted);
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--relative-time"
Values
| Name | Description | Attribute |
|---|---|---|
locale
|
Locale for Intl.RelativeTimeFormat; falls back to the element's lang then the document's; empty by default. | data-stimeo--relative-time-locale-value |
threshold
|
Seconds past which the relative text reverts to the authored absolute text; 0 disables (default 0). | data-stimeo--relative-time-threshold-value |
tickInterval
|
Minimum polling interval in milliseconds, widened for coarser units (default 60000). | data-stimeo--relative-time-tick-interval-value |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
text content |
Root element | The relative phrase (e.g. "3 minutes ago"). |
datetime |
Root element | The machine-readable absolute time (immutable). |
data-state |
Root element | "relative" / "absolute" (past the threshold). |