Local Time
stimeo--local-time
Renders a UTC timestamp as an absolute, viewer-localized time via Intl.
The stimeo--local-time controller follows the HTML <time> semantics. It reads the UTC datetime attribute and reformats the visible text into the viewer's locale and timezone with Intl.DateTimeFormat — a browser standard, no added dependency. The server can emit plain UTC (it never needs the viewer's timezone, so the markup stays cacheable) while the client localizes the display. Distinct from Relative Time's "3 minutes ago", this is the absolute-localization axis. The machine-readable datetime is left untouched so assistive tech and crawlers keep the canonical value, and an invalid datetime/timezone leaves the authored text in place. Formatting is a pure function of datetime, so a Turbo cache restore or morph re-runs it and stays consistent. Behavior only — styling is owned by this Playground.
- Published UTC 2026-06-08 12:30
- Released UTC 2026-01-01 09:00
- Starts UTC 2026-12-24 21:00
- Now UTC 2026-06-21 19:44
Each time is stored as UTC in the datetime attribute and localized in your browser to your locale and timezone, so two visitors in different zones see different clock times for the same instant. The machine-readable datetime stays fixed; hover any time to see a more detailed form in the title.
<%# Markup for the local-time demo.
Each <time> carries a machine-readable UTC datetime; the controller reformats the
visible text into the viewer's locale and timezone via Intl.DateTimeFormat, leaving
the datetime attribute (the screen-reader / SEO value) intact. The locale follows the
page language and the timezone follows the browser, so the server can emit plain UTC
and stay cacheable. The last row clears the time style to show a date-only format. %>
<% rows = [
{ at: Time.utc(2026, 6, 8, 12, 30), key: "published", date: "medium", time: "short" },
{ at: Time.utc(2026, 1, 1, 9, 0), key: "released", date: "full", time: "short" },
{ at: Time.utc(2026, 12, 24, 21, 0), key: "starts", date: "long", time: "" },
{ at: Time.now.utc, key: "now", date: "medium", time: "medium" }
] %>
<ul class="local-time-demo">
<% rows.each do |row| %>
<li class="local-time-demo__row">
<span class="local-time-demo__label">
<%= t("components.local_time.demo.#{row[:key]}") %>
</span>
<span class="local-time-demo__convert">
<span class="local-time-demo__source">UTC <%= row[:at].strftime("%Y-%m-%d %H:%M") %></span>
<span class="local-time-demo__arrow" aria-hidden="true">→</span>
<time
class="local-time"
data-controller="stimeo--local-time"
datetime="<%= row[:at].iso8601 %>"
data-stimeo--local-time-locale-value="<%= I18n.locale %>"
data-stimeo--local-time-date-style-value="<%= row[:date] %>"
data-stimeo--local-time-time-style-value="<%= row[:time] %>"
data-stimeo--local-time-title-format-value="long">
<%= row[:at].strftime("%Y-%m-%d %H:%M UTC") %>
</time>
</span>
</li>
<% end %>
</ul>
<p class="local-time-demo__hint"><%= t("components.local_time.demo.hint") %></p>
/*
* Presentation-only styles for the local-time demo.
* The library only replaces the element's text with the localized absolute time
* (and adds a detailed title); the machine-readable datetime attribute is kept.
*/
.local-time-demo {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
max-width: 46rem;
}
.local-time-demo__row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
/* Keep the label and value on one line when there's room; on a genuinely narrow
stage the value group drops to a second line as a whole — never mid-value
(see the nowrap rules below), which is what made English look cluttered. */
flex-wrap: wrap;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
}
.local-time-demo__label {
color: var(--muted);
font-size: 0.9rem;
}
.local-time-demo__convert {
display: flex;
align-items: baseline;
gap: 0.5rem;
white-space: nowrap;
}
/* The source UTC, shown next to the localized time so the conversion is verifiable. */
.local-time-demo__source {
color: var(--muted);
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.local-time-demo__arrow {
color: var(--muted);
}
/* The localized absolute time. An English full date is long, so keep it on one
line and let the row (not the time) wrap when space is tight. */
.local-time {
font-variant-numeric: tabular-nums;
color: var(--fg);
white-space: nowrap;
}
/* Explanatory caption: the time is localized on the client from the UTC datetime. */
.local-time-demo__hint {
margin: 0.75rem 0 0;
max-width: 46rem;
font-size: 0.85rem;
line-height: 1.5;
color: var(--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--local-time"
Values
| Name | Description | Attribute |
|---|---|---|
locale
|
Locale for Intl.DateTimeFormat; falls back to the element's lang then the document's; empty by default. |
data-stimeo--local-time-locale-value |
timeZone
|
IANA timezone; empty uses the browser default. | data-stimeo--local-time-time-zone-value |
dateStyle
|
Intl dateStyle (full/long/medium/short); empty omits the date (default medium). |
data-stimeo--local-time-date-style-value |
timeStyle
|
Intl timeStyle (full/long/medium/short); empty omits the time (default short). |
data-stimeo--local-time-time-style-value |
titleFormat
|
Intl style applied to both date and time for the title; empty adds no title (default empty). | data-stimeo--local-time-title-format-value |
Events
| Name | Description | Event |
|---|---|---|
format
|
Fires after formatting with detail.formatted. |
stimeo--local-time:format |
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 localized absolute time. |
title |
Root element | An optional detailed form (when titleFormat is set). |
datetime |
Root element | The machine-readable UTC value (immutable). |