Local Time
stimeo--local-time
Shows a UTC timestamp from your server in the viewer's own timezone and language.
Your server emits plain UTC and only the display is adapted to the viewer's timezone and language. Since the server never needs to know where the reader is, the HTML it generates stays cacheable. The formatting uses what the browser already provides, with no extra dependency. The machine-readable value is left untouched, so what screen readers and crawlers see stays correct. A time or zone it cannot read is left showing as it was. For wording like "3 minutes ago", use the relative time part instead.
- Published UTC 2026-06-08 12:30
- Released UTC 2026-01-01 09:00
- Starts UTC 2026-12-24 21:00
- Now UTC 2026-09-20 05:46
Every time here is stored as UTC and only displayed in your timezone and language, so the same instant reads as a different clock time elsewhere. Hover one to see a fuller form.
Keyboard
This component has no keyboard interactions of its own.
<%# 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). |