Skip to content

Localized time formatting without JavaScript #12591

Open

Description

@lucacasonato

What problem are you trying to solve?

Server side rendered (or statically rendered) HTML does not know what the user’s timezone is. This means you can not server render absolute time correctly. To workaround this, the following are common on the web platform today:

  • Use relative time
    • Instead of “June 14th 2026”, say “2 days ago”
    • Does not work with caching / prerendering or anything else that isn’t per request
  • Show and specify one specific timezone
    • Show “June 16th 2026 10:00 CEST” to all users
    • Users have to do the timezone conversions themselves
  • Guess timezone based on IP / locale metadata
    • What could go wrong??? (Borders, very imprecise)
  • Use JavaScript
    • Needs synchronous <script> tag to avoid flash of bad content, hard in many frameworks and bad for perf

We discussed this problem and possible solutions briefly at Web Engines Hackfest 2025, and then at length in a breakout session at Web Engines Hackfest 2026. There was wide consensus among vendors that this is an issue that should be resolved.

How would you solve it?

Considerations

During discussion, the following considerations for the solution were identified:

  • Web developer provides a machine readable timestamp à la RFC 9557
  • Web developer optionally provides formatting attributes (e.g. 12/24 hours), and locale
  • For old browsers or in case of missing locales, a fallback text should be used from the original HTML text
  • An HTML element with attributes should be used as the trigger to enable this behaviour. The existing <time> element looks like a logical contender to be this element, as it does not have behaviour yet.
  • Engine implementors consider solutions that involve replacing DOM content (innerText) problematic for various reasons (performance, timing compat risk)
  • Engine implementors favor solutions using either CSS content or a UA shadow root, as these are sufficiently user unobservable to mitigate the risks identified with DOM content replacement
  • Using CSS content would require significant additional work to make text selection (and DOMRange integration) work
  • The available formatting surface should be a subset of what is available through Intl.DateTimeFormat. We should not introduce new capabilities, particularly in terms of user preferences, as this can affect privacy.
  • Absolute time formatting was identified as the most important use case. We expect that (live) relative time formatting (2 seconds ago etc) is also an important use case.
  • The ability to individually style certain parts of the date / time string were identified as "nice to have".

Proposed solution

We propose to extend the <time> element with the capability to render a formatted absolute date, time, or date time instead of it's child text node when the user opts into this behaviour using an attribute. We propose that the formatted value is rendered through a UA shadow root.

<time datetime="2026-06-17T08:13:41.099Z" format="datetime">
  (fallback display)
</time>

The trigger to enable this behaviour would be a new format attribute on the <time> element, that can be set to either "date", "time", or "date time".

Additionally more attributes are available to customise the formatting of the date / time / date time:

  • datefields: enumerated value of "weekday", "day-weekday", "month-day", "month-day-weekday", "year-month", "year-month-day" (default), "year-month-day-weekday". This controls which parts of the date are displayed.
  • datelength: enumerated value of "long" (default), "medium", "short". Controls how the parts of the date are formatted.
  • timeprecision: enumerated value of "hour", "minute", "second" (default). Controls to which granularity time is shown.
  • timezonestyle: enumerated value of "long", "short", "" (default). Controls whether and how the timezone is displayed.
  • hour12: boolean attribute with "" default. Controls whether 12 hour time or 24 hour time, or locale default are used.
  • calendar: enumerated value of calendars available through AvailableCalendars() AO in ECMA 402, or "input" (default). Controls what calendar the date should be displayed in.
    • "input" indicates to use the calendar of the provided date time.
  • timezone: string representing a timezone identifier, the string "input", or the string "" (default). Controls what timezone to convert the provided datetime into for display.
    • "input" indicates to use the timezone of the provided date time. "" indicates to use the UA's current timezone.
  • an attribute to switch between 24 and 12 hour time display (name TBD). Tri-state for 12, 24, or locale default.

To determine the locale, we walk the DOM to find the closest ancestor with a lang attribute (this may be the <time> element itself). When the lang changes, the formatting code should be re-run.

To parse the datetime attribute into a "machine readable equivalent", we use the machine-readable equivalent of the element's contents algorithm. An additional algorithm will be needed to transform the return value of that algorithm into Temporal values for use with ECMA 402's formatting algorithms. Additionally the algorithm will have to be modified to support ZonedDateTime and calendar annotations from RFC 9557 (see also).

The result of parsing the datetime attribute can be one of Temporal.PlainDateTime, Temporal.PlainTime, Temporal.PlainDate, Temporal.PlainYearMonth, or Temporal.PlainMonthDay, Temporal.Instant or Temporal.ZonedDateTime.

The timezone=input option is only supported for Temporal.ZonedDateTime. When the value of timezonestyle is not the empty string, timezone="" is only supported for Temporal.Instant and Temporal.ZonedDateTime.

If a Temporal.ZonedDateTime is provided, the value should be converted to the specified timezone using withTimeZone() and to the specified calendar using withCalendar(). If a Temporal.Instant is provided, it should be converted to a Temporal.ZonedDateTime using toZonedDateTimeISO() and to the specified calendar using withCalendar().

The attribute values map to ECMA-402 formatting options through the following JS algorithm:

const options: Intl.DateTimeFormatOptions = {}; 

if (format === "date" || format === "datetime") {	
  const dateFields = new Set(datefields.split("-"));
  if (dateFields.has("year")) options.year = "numeric";
  if (dateFields.has("month")) {
    options.month =
      dateLength === "long"
        ? "long"
        : dateLength === "short"
          ? "numeric"
          : "short";
  }
  if (dateFields.has("day")) options.day = "numeric";
  if (dateFields.has("weekday")) {
    options.weekday = dateLength === "long" ? "long" : "short";
  }
}
if (format === "time" || format === "datetime") {
  switch (timeprecision) {
    case "hour":
      options.hour = "numeric";
      break;
    case "minute":
      options.hour = "numeric";
      options.minute = "numeric";
      break;
    case "second":
      options.hour = "numeric";
      options.minute = "numeric";
      options.second = "numeric";
      break;
  }
}
if (timezonestyle !== "") {
  options.timeZoneName = timeZoneStyle === "long" ? "long" : "short";
} 

The time is formatted through Intl.DateTimeFormat#formatToParts. The resulting parts are converted to inline span elements in the UA shadow root with a type such as "weekday", "literal", or "month". CSS pseudo element selectors then allow selecting these span elements based on the type.

Q&A

Why a shadow root instead of CSS content? It makes selection much more straightforward, and enables individual styling for certain parts of the date / time.

Where do the attribute names come from? The attribute names and time skeletons come from Unicode MessageFormat 2 (in technical preview). These are not available directly in ECMA-402 yet, but we are planning to add these to ECMA-402 in due time also. They are a subset of the capabilities of ECMA-402 currently (just different syntax).

Activity

  1. karlhorky commented on Jun 17, 2026

    @karlhorky

    Great proposal 🙌 Would be very cool to see something like this in the web platform.

    To workaround this, the following are common on the web platform today:

    Does it make sense to also add another point to the common web platform patterns?

    • Send the user's time zone to the server
      • Does not work with statically rendered HTML
      • Passive client hint could be used for fingerprinting

    from prior art like this:

  2. annevk commented on Jun 17, 2026

    @annevk
    Member

    cc @whatwg/i18n

  3. added
    i18n-trackerGroup bringing to attention of Internationalization, or tracked by i18n but not needing response.
    on Jun 17, 2026
  4. dbaron commented on Jun 17, 2026

    @dbaron
    Member

    One thought is that it's worth thinking about how to test this well in web-platform-tests. In particular, I think we could use (if we don't have it already) some ability to run some web platform tests as though the user's timezone and locale are something in particular (which also means being able to describe those timezones and locales).

  5. annevk commented on Jun 17, 2026

    @annevk
    Member

    I think we want the element to follow the element's language, so for this element we can probably rely on reference tests alone.

  6. added
    needs testsMoving the issue forward requires someone to write tests
    needs concrete proposalMoving the issue forward requires someone to figure out a detailed plan
    on Jun 17, 2026
  7. eemeli commented on Jun 17, 2026

    @eemeli
    Member

    Testing this should eventually be easier once https://github.com/tc39/proposal-stable-formatting advances. It's looking to provide a locale for which the formatted output is defined directly within the JS specification, and which will not change.

  8. xfq commented on Jun 18, 2026

    @xfq
    Contributor

    hour12 as a boolean attribute is probably not enough for the stated tri-state need? HTML boolean attributes can represent true/false, but probably not 'default'. Maybe use an enumerated attribute aligned with ECMA-402 hour cycle concepts, or defer it?

  9. justingrant commented on Jun 18, 2026

    @justingrant

    I'm one of the Temporal champions and have a few questions:

    timezone: string representing a timezone identifier, the string "input", or the string "" (default). Controls what timezone to convert the provided datetime into for display.

    • "input" indicates to use the calendar of the provided date time. "" indicates to use the UA's current timezone.

    Looks like a copy/paste mistake. I think you mean "timezone of the provided date time" not "calendar...".

    Also, the "input" vs. "" choice is interesting. Another option could use the type of the input: if the input contains a time zone annotation like [America/New_York] then the time zone of the input could be used, otherwise it'd rely on the time zone of the user agent. Why did you decide against this option?

    Related: if we do end up using "input", then what will happen if timezone="input" but the input lacks a time zone annotation?

    The same questions apply to calendars, but with a twist: all non-Instant datetime types in Temporal have a calendar, but if the calendar is iso8601 then no calendar annotation is shown. But given that we consider iso8601 to be a "null calendar" for the purpose of localized formatting (meaning the UA's default calendar should be used), then the same "if a calendar annotation is present then use it, otherwise use the UA's calendar" option could apply as discussed above with time zones.

    The attribute values map to ECMA-402 formatting options through the following JS algorithm:

    I didn't see the 12 vs. 24 hour format shown in the algorithm.

    • hour12: boolean attribute with "" default. Controls whether 12 hour time or 24 hour time, or locale default are used.
    • an attribute to switch between 24 and 12 hour time display (name TBD). Tri-state for 12, 24, or locale default.

    Which of these two redundant bullet points are correct?

    Also, why did you choose to use a boolean vs. the four choices of hourCycle? Not saying this was a bad choice, but would be good to document the reasoning.

  10. eemeli commented on Jun 21, 2026

    @eemeli
    Member

    On hour12 vs. hourCycle, here's a good summary of how/why we ended up with only the former in Unicode MessageFormat: unicode-org/message-format-wg#1097 (comment)

    The short answer is that h24 is not actually used by any locale (and might even get deprecated, see tc39/ecma402#1002), and that the choice between h11 and h12 is locale-dependent. This leaves the true/false/undefined values of hour12 as the three options that we do actively want to support, and the option name "hour12" as the least worst option we've come across so far. Recommendations are quite welcome for improvements; unicode-org/message-format-wg#1097 is probably the best place for such.

    The specifics of timeZone and its input value are documented here.

  11. ansuz commented on Jun 22, 2026

    @ansuz

    To workaround this, the following are common on the web platform today:

    Does it make sense to also add another point to the common web platform patterns?

    * Send the user's time zone to the server
      
      * Does not work with statically rendered HTML
      * Passive client hint could be used for fingerprinting
    

    While I assume this is proposed with good intentions, it seems it would likely just end up as yet another data-point to be exploited by data-brokers. I much prefer OP's purely client-side solution.

  12. karlhorky commented on Jun 22, 2026

    @karlhorky

    @ansuz I think you're misunderstanding - my comment was not proposing an alternative to be evaluated or compared against this proposal.

    My comment was proposing that @lucacasonato add a point to the list in the issue description: specifically, the "To workaround this, the following are common on the web platform today:" quote list.

  13. ansuz commented on Jun 22, 2026

    @ansuz

    @karlhorky thanks for clarifying

  14. andreas-taranetz commented on Jun 22, 2026

    @andreas-taranetz

    One use case this proposal doesn't seem to cover: Displaying a time in the timezone the user was in when the event occurred, rather than their current timezone.

    This could differ due to daylight savings time.

    I faced this usecase when showing a history of transactions.
    The timezone attribute could be extended to support this as well.

  15. hsivonen commented on Jun 22, 2026

    @hsivonen
    Member

    One use case this proposal doesn't seem to cover: Displaying a time in the timezone the user was in when the event occurred, rather than their current timezone.

    This could differ due to daylight savings time.

    I faced this usecase when showing a history of transactions. The timezone attribute could be extended to support this as well.

    "Timezone" should be understood to be a city and not an UTC offset, and then this works.

  16. justingrant commented on Jun 22, 2026

    @justingrant

    "Timezone" should be understood to be a city and not an UTC offset, and then this works.

    By "city" I assume you mean an IANA Time Zone Database name like Europe/Paris, correct? If so then great.

  17. lucacasonato commented on Jun 23, 2026

    @lucacasonato
    MemberAuthor

    @andreas-taranetz This is a supported use case already! What you can do is provide a stringified Temporal.ZonedDateTime in datetime, which includes a timezone such as Europe/Amsterdam (e.g. 2026-06-23T20:37:07.255570068+02:00[Europe/Amsterdam]). By default, this would be converted to the user's local timezone and displayed. However, when you specify timezone=input, it will instead be displayed in the timezone specified inside the ZonedDateTime string. This option is only supported for ZonedDateTime strings. Example:

    <time datetime="2026-06-23T20:37:07.255570068+02:00[Europe/Amsterdam]" format="datetime" timezone="input">fallback</time>

    This matches the behaviour of :datetime in Message Format 2.

  18. lucacasonato commented on Jun 23, 2026

    @lucacasonato
    MemberAuthor

    @justingrant

    Also, the "input" vs. "" choice is interesting. Another option could use the type of the input: if the input contains a time zone annotation like [America/New_York] then the time zone of the input could be used, otherwise it'd rely on the time zone of the user agent. Why did you decide against this option?

    Because it would change the behaviour based on whether a user uses an Instant (converted to local time for display), or a ZonedDateTime (not converted to local time), or a plain date/time/datetime (which is assumed to be in local timezone for formatting). If the default is "always display in local time", then all three behave the same.

    I expect it to be common for ZonedDateTime to be used relatively interchangeably with Instant, with some users preferring ZonedDateTime for it's arithmetic capabilities rather than the fact that it is pinned to any time zone.

    Message Format 2 independently came to the same conclusion about default behaviour here, which suggests to me that it is the correct one.

    Related: if we do end up using "input", then what will happen if timezone="input" but the input lacks a time zone annotation?

    Formatting fails and the fallback is used. It's like specifying an invalid timezone, or an invalid date time string, or an invalid option.

    The same questions apply to calendars, but with a twist: all non-Instant datetime types in Temporal have a calendar, but if the calendar is iso8601 then no calendar annotation is shown. But given that we consider iso8601 to be a "null calendar" for the purpose of localized formatting (meaning the UA's default calendar should be used), then the same "if a calendar annotation is present then use it, otherwise use the UA's calendar" option could apply as discussed above with time zones.

    I was going to answer to that "because iso8601 doesn't format usefully, so the default should not be that", but I am sympathetic to your point regarding considering iso8601 to be "null" and thus defaulting it to UA calendar. That does pose a weird problem though: if you explicitly specify iso8601 as the calendar on your datetime, then it will still get defaulted to the UA calendar unless you specify calendar=iso8601 explicitly.

    We should think about this case some more. I see the following options:

    • Always convert to UA calendar, except when calendar=input or calendar=<id> is set.
    • Convert iso8601 to UA calendar, except when calendar=<id> is set. Needs an option to force to UA calendar?

    I still lean towards the first, because I think it is also what MF2 does. Also it is the least difficult to explain.

  19. Masstronaut commented on Jun 24, 2026

    @Masstronaut

    I think it would be nice to support date time string format strings for specifying how the time should be rendered:

    • they are already a familiar pattern for formatting date time values as strings on the web
    • the format is less verbose than specifying many attributes
    • it’s easy to tell at a glance what the final rendered date time string will look like
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsi18n-trackerGroup bringing to attention of Internationalization, or tracked by i18n but not needing response.needs concrete proposalMoving the issue forward requires someone to figure out a detailed planneeds implementer interestMoving the issue forward requires implementers to express interestneeds testsMoving the issue forward requires someone to write testsstage: 1Incubation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions