Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: David Phipps
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: David Phipps

Formatting Dates In The Local Timezone With Alpine.js

By
Published in Comments (12)

On a recent episode of Syntax.fm, Wes Bos, Scott Tolinski, and guest Scott Jehl discussed the current landscape of "Web Components" (aka, custom elements). I haven't worked with custom elements directly; but, I have been playing around a lot with Alpine.js. And, one thing that Jehl mentioned that caught my ear was the use of custom elements to format date/time values on the client-side. I wanted to see what it might look like to perform this task with an Alpine.js component.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Formatting date/time values in a user-facing application is tricky because dates are typically stored in UTC time and then rendered in various timezones around the world. So, a given date that is 11am for one user might actually be 3am for a different user. Which means, although it might be easiest to just format a date/time value as-is, it's often not the best experience for the users.

To translate date/time values into a user's local timezone, we can either perform the translation on the server-side using a preference setting; or, we can parse a date/time value in the browser, which will automatically translate it into the user's local timezone, and then render it on the client-side. In this experiment, we're going to be doing a little of both.

There is no server-side rendering in this experiment; but, let's assume that we have a server-side rendered template which will be progressively enhanced by Alpine.js on the client-side. When the server-side renders a date, it will do so using the UTC time:

Nov 20, 2024

But, it will wrap the rendered date in an HTML <time> element in order to provide the original date (in UTC) and the date mask which it used to format the server-side date:

<time datetime="..." data-mask="...">Nov 20, 2024</time>

Then, we'll apply an x-data attribute to bind this <time> element to an Alpine.js component. This Alpine.js component will parse the datetime attribute into a local Date instance; and replace the textContent of the host element with a formatted date-string using the given mask.

To be clear, this isn't intended to be an Internationalization (I18n) demo—the mask that I'm using here isn't locale-specific. This is only an exploration of using Alpine.js to rerender dates in the local timezone.

With that said, here's a proof-of-concept. There are two different dates, each of which will receive a separate instance of the Alpine.js component.

<!doctype html>
<html lang="en">
<body>

	<h1>
		Formatting Dates In The Local Timezone With Alpine.js
	</h1>

	<!--
		In the following examples, let's assume that the already-rendered date string was
		rendered on the server using the UTC timezone.
	-->
	<p>
		<time
			x-data="LocalDateFormat"
			data-mask="mmmm d, yyyy 'at' HH:mmtt"
			datetime="2024-11-17T16:38:48Z">
			Nov 17, 2024 UTC <!-- Formatted in UTC. -->
		</time>
	</p>
	<p>
		<time
			x-data="LocalDateFormat"
			data-mask="mmmm d, yyyy 'at' HH:mmtt"
			datetime="2024-11-20T14:40:42.832Z">
			Nov 20, 2024 UTC <!-- Formatted in UTC. -->
		</time>
	</p>

	<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
	<script type="text/javascript">

		/**
		* This Alpine.js component replaces the text-content of the host element with a
		* date-string formatted in the user's local timezone. It does this by parsing the
		* datetime attribute into a local Date object and then re-masking it.
		* 
		* Note: This is not an Internationalization technique - it doesn't use the Intl
		* module, though I'm sure it could be updated to do so. This is more of a higher-
		* level exploration client-side date-formatting.
		*/
		function LocalDateFormat() {

			// In a <time> element, the "datetime" attribute is intended to represent a
			// period in time. For the sake of this demo, I'm going to assume that the
			// attribute contains a full UTC date/time value.
			var date = new Date( this.$el.getAttribute( "datetime" ) );
			var mask = this.$el.dataset.mask;

			// In order to translate the server-side date formatting into a client-side
			// context in the user's local timezone, this Alpine.js component expects a
			// date mask to be provided as a data-attribute. The text content of the host
			// will be replaced with the interpolation of the date parts and the mask.
			this.$el.textContent = mask.replace(
				/'([^']*)'|y+|m+|d+|H+|h+|n+|s+|T+|t+/g,
				( $0, $1 ) => {

					// Return escaped string (less the surrounding quotes).
					if ( $1 ) {

						return $1;

					}

					return translations[ $0 ]( date );

				}

			);

		}

		// Utility methods for applying mask parts to a given date.
		var translations = {
			yyyy: ( date ) => String( date.getFullYear() ),
			yy: ( date )   => String( date.getYear() - 100 ), // Deprecated, never use short year.
			mmmm: ( date ) => String( monthNames[ date.getMonth() ].long ),
			mmm: ( date )  => String( monthNames[ date.getMonth() ].short ),
			mm: ( date )   => String( date.getMonth() ).padStart( 2, "0" ),
			m: ( date )    => String( date.getMonth() ),
			dddd: ( date ) => String( dayNames[ date.getDate() ].long ),
			ddd: ( date )  => String( dayNames[ date.getDate() ].short ),
			dd: ( date )   => String( date.getDate() ).padStart( 2, "0" ),
			d: ( date )    => String( date.getDate() ),
			HH: ( date )   => String( date.getHours() ).padStart( 2, "0" ),
			H: ( date )    => String( date.getHours() ),
			hh: ( date )   => String( 12 % date.getHours() ).padStart( 2, "0" ),
			mm: ( date )   => String( date.getMinutes() ).padStart( 2, "0" ),
			m: ( date )    => String( date.getMinutes() ),
			ss: ( date )   => String( date.getSeconds() ).padStart( 2, "0" ),
			s: ( date )    => String( date.getSeconds() ),
			TT: ( date )   => String( date.getHours() >= 12 ? "PM" : "AM" ),
			tt: ( date )   => String( date.getHours() >= 12 ? "pm" : "am" )
		};
		var monthNames = [
			{ short: "Jan", long: "January" },
			{ short: "Feb", long: "February" },
			{ short: "Mar", long: "March" },
			{ short: "Apr", long: "April" },
			{ short: "May", long: "May" },
			{ short: "Jun", long: "June" },
			{ short: "Jul", long: "July" },
			{ short: "Aug", long: "August" },
			{ short: "Sep", long: "September" },
			{ short: "Oct", long: "October" },
			{ short: "Nov", long: "November" },
			{ short: "Dec", long: "December" }
		];
		var dayNames = [
			{ short: "Sun", long: "Sunday" },
			{ short: "Mon", long: "Monday" },
			{ short: "Tue", long: "Tuesday" },
			{ short: "Wed", long: "Wednesday" },
			{ short: "Thr", long: "Thursday" },
			{ short: "Fri", long: "Friday" },
			{ short: "Sat", long: "Saturday" }
		];

	</script>

</body>
</html>

There's no error handling in this code; and, it's not intended to be exhaustive in its options. I only wanted to explore the concept. And, when we run this code, we end up with the following output:

As you can see, the textContent of each <time> element has been updated to render the given UTC date in my local timezone using the given date mask.

If the JavaScript were to fail to load or error-out, the user would still see a date with the server-rendered UTC labeling. But, once the JavaScript kicks-in, they will see the date in their own timezone (and without the UTC label).

Obviously this only works in a web-based context where Alpine.js can run; so, it wouldn't be applicable for tasks like document generation or exports. But, it's a very compelling notion for a web application.

Want to use code from this post? Check out the license.

Reader Comments

362 Comments

One word of caution for this approach. I was on a trip to CA once, attending a conference, and looking at their schedule. The web page automatically changed the times to my tz, and since it was very close (2 hours difference), I didn't know, and, I assumed the times were in PST as it was a CA conference. I almost missed things because of this. If you adjust times, I'd find some UX/UI way of letting the user know this.

15,902 Comments

@Raymond,

That's a good point, especially in contexts that deal with scheduling where a misunderstanding could become a problem. For more passive contexts (like the date a comment was made or the date a PR was opened), the risk a problem is considerably less.

In the case of a physical world scheduling issue, I wonder if it would make sense to just always show the times in the local timezone of the event. Kind of like how flight plans work - it usually shows the take-off / landing times in the local timezone (which is always run to realize that your 11am-2pm flight is actually like 7 hours long 😂).

247 Comments

@Ben, @Ray,

Ray makes a really great point. I agree that where schedules are concerned, it makes sense to render the dates in the timezone that in-person event will take place. If it were a virtual event, then local time makes sense. Either way, it probably makes sense to add the timezone designation to the date/time output to eliminate any ambiguity about which timezone is reflected. So UTC / EST / CST / PST / etc.

15,902 Comments

Yeah, especially with scheduling workflows. That's part of why I'm so drawn to the 3 hours ago kind of label where it makes sense; you don't have to worry as much about relative dates because it's more fuzzy. Of course, that doesn't make sense for something like a conference.

24 Comments

I've moved to using the Intl.DateTimeFormat for all of my date/time formatting. Native in all modern browsers, and can handle timezone conversions for me when needed. Plus the formatting that the Temporal API will use is the same under the hood.

15,902 Comments

@Cutter,

I still need to dig into it; but at first blush, I was hoping the API would be much simpler. What I had in my mind was that I could provide it some masks and then have it do the "right thing" in the user's locale context.

Meaning, I wish I could just give it something like yyyy-mm-dd, and it would use that for US contexts, and then maybe automatically use something like mm-dd-yyyy for European context (or whatever format they use). Instead, it seems (again at first blush) to use a much more cryptic setting object.

I'm sure once I dig in deeper, it will make more sense. But, just skimming through the MDN docs, it felt like it was going to be more complicated than I had hoped.

362 Comments

@Ben Nadel,
I would argue that it isn't cryptic at all, maybe a bit complex in terms of all the options. It does NOT, however, support passing string masks. It does, however, support a huge number of formatting options.

Yes, I'm defending my baby. ;)

247 Comments

@Ben,

I took a quick look at the MDN Docs as well and based on the following example, I think it would actually be as simple as you hoped. If I'm reading this right, it would use the user's local settings to define the default time zone and then just do the right thing. No?

const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));

// toLocaleString without arguments depends on the implementation,
// the default locale, and the default time zone

console.log(new Intl.DateTimeFormat().format(date));
// "12/19/2012" if run with en-US locale (language) and time zone America/Los_Angeles (UTC-0800)
24 Comments

@Ben,

You can build your own formatter function to use the users local 'locale', and it will automatically adjust accordingly.

function buildFormatter() {
  const {locale} = new Intl.DateTimeFormat().resolvedOptions();
  const formatter = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit'
  });
  return (date) => {
    if (!date) {
      return '';
    }
    return formatter.format(new Date(date));
  }
}

const myFormatter = buildFormatter();
console.log(`My date is ${myFormatter('2024-12-25T10:15:00Z')}`)

This allows you to create a consistent display output (though you might want some code to validate your date). You don't supply a string output, as it automatically uses standardized locale specific date/time output based upon the options you provide.

If you want to play around some https://github.com/cutterbl/js-date-time-play

15,902 Comments

@Cutter,

That looks really cool actually. Thanks for putting that together. I hadn't even seen the resolvedOptions() method before. What I'm gathering here is that if you instantiate the Intl.DateTimeFormat() without any settings, it will use the user's baseline browser settings? And then you can use that instance to extract those for use as inputs later on? Pretty clever!

@Chris,

I do think it might do the right thing—I think I'm just getting comfortable with the idea of giving up the easy "mask"-based world that I've been living in for so long 🙃

24 Comments

@Ben Nadel,

Yeah, some of my examples there are bad, only because the Temporal.ZonedDateTime.toLocaleString() is a wrapper around Intl.DateTimeFormat() (something I discovered after that preso). But loving that this stuff is now becoming browser native. For base formatting today the Intl.DateTimeFormat is the way to go, but once Temporal starts hitting browsers it will change how we handle dates going forward. (That polyfill comes from the working group). For those of us building global use scheduling applications these things are absolute game changers, removing things like Moment and DayJs from our codebase.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel