Skip to main content

/ Treasa Ní Chonchúir

Time zones and the <time> element

When you construct a JavaScript Date object, the resulting Date represents the time you intended. Most of the time. But sometimes it doesn’t.

Consider the following examples, which I ran on a computer set to Pacific Daylight Time (UTC-07:00):

A time zone is explicitly provided
% jsc
>>> new Date("1979-08-22T08:43:00-04:00")
Wed Aug 22 1979 05:43:00 GMT-0700 (Pacific Daylight Time)

In this first example, I explicitly provided a time in Eastern Daylight Time (UTC-04:00). My computer’s time zone is three hours off of that, so when the Date gets printed, we see that 8:43 in the morning in Eastern Time is 5:43 in the morning, Pacific Time.

Same input but without an explicit time zone
>>> new Date("1979-08-22T08:43:00")
Wed Aug 22 1979 08:43:00 GMT-0700 (Pacific Daylight Time)

If we provide the same time but omit the time zone, the Date constructor assumes you mean the computer’s local time zone.

Other input formats without time zones (one of these things is not like the other)
>>> new Date(1979, 8-1, 22, 5, 43, 0)
Wed Aug 22 1979 05:43:00 GMT-0700 (Pacific Daylight Time)
>>> new Date(1979, 8-1, 22)
Wed Aug 22 1979 00:00:00 GMT-0700 (Pacific Daylight Time)
>>> new Date("1979-08-22")
Tue Aug 21 1979 17:00:00 GMT-0700 (Pacific Daylight Time)

Here you can see that the Date constructor takes a number of different input formats. None of these cases contain an explicitly-set time zone. And, as in the previous example, all but one of these uses the computer’s local time zone. But one of them doesn’t! In the last case, we provide a simple date—with no time at all—in ISO 8601 format. In this case, and only this case, the Date constructor assumes you mean UTC.

I really wouldn’t mind—the web has accumulated many other, far worse, inconsistencies over time—except this one trips me up all the goddamn time on my own simple, static web site. How?

Well, when I write a blog post, I use the <time> element (the HTML element that augments a human-readable date or time with a machine-readable representation of the same time) to mark up the date the post was published. Since I write these by hand, and I rarely post more than once a day, I generally don’t bother to get more specific than just the date.

For instance, consider the post you’re reading right now. I published it on . The markup for that looks like this:

This post’s publish date
<time datetime=2024-10-27>October 27<sup>th</sup>, 2024</time>

I’m sure you see where this is going. When I pass the contents of the <time> element’s datetime attribute to the Date constructor for use inside my static site generator, my SSG thinks the post got published the day before it actually went out. (Midnight UTC is the previous evening in California.)

I have been meaning to fix this for ages, knowing full well it’d only take a few lines of JavaScript, but I didn’t get around to it until the other day. Assuming you have elTiempo, a <time> element whose datetime attribute looks like YYYY-MM-DD, and maybePrependZero, an Intl. configured to format single digit integers with a leading zero, here’s the fix:

Fix a <time> element’s time zone
const tzOffset = new Date(elTiempo.dateTime).();
const tzHours = -Math.floor(tzOffset / 60);
const tzMinutes = tzOffset % 60;

let fixedDateTime = elTiempo.dateTime + "T00:00:00";
if (tzHours > 0) {
    fixedDateTime += "+";
}
fixedDateTime += maybePrependZero.format(tzHours) + ":";
fixedDateTime += maybePrependZero.format(tzMinutes);

elTiempo.dateTime = fixedDateTime;

But we want to fix every <time> element on the page, of course, not just one of them.

Fix the time zone of every <time> element on the page
Create a number formatter for ±HH:MM time zone offsets
for (const elTiempo of document.("time")) {
    Skip irrelevant <time> elements
    Fix a <time> element’s time zone
}

We only need to fix <time> elements whose datetime attribute matches ’s full-date production, YYYY-MM-DD.

Skip irrelevant <time> elements
if (! /^\d{4}-\d{2}-\d{2}$/.test(elTiempo.dateTime)) {
    continue;
}

All of the above code assumes maybePrependZero exists.

Create a number formatter for ±HH:MM time zone offsets
const maybePrependZero = Intl.NumberFormat("en-US", {
    style: "decimal",
    minimumIntegerDigits: 2
});