xinjs <timezone-picker>

For some reason I’ve had a nagging urge to build a nice timezone selector ever since I started working on a calendaring system for a previous startup and we needed a timezone selector (in the end we just had a popup selector for US timezones and left the difficult stuff for later).

Way back in the day, it used to be pretty normal to pick your timezone manually in your computer preferences. For years, Apple had what I considered to be the best designed timezone selector I’d ever used. That said, I didn’t live in one of the places where this can be an issue (e.g. there are places you can live where web-browsers don’t recognize your timezone as existing…).

Back when I was working on my timezone picker, I experimentally contracted an artist to build me an SVG file with the timezones as different shapes. I provided the artist with a video capture of the Apple timezone picker (as it existed then) as a reference.

Anyway, getting anything close to what I wanted was a nightmare, in large part because the natural inclination of any artist when asked to produce a map is to cheat and try to grab one someone else has gone and adapt it.

From experience, I know this is a terrible idea, since most vector map artwork is bad AND crazy heavy for UI work. You don’t need Norwegian fjords or the Indonesian archipelago to be rendered correctly. You just need something that is recognizable enough to let you click in the right spot.

Anyway, getting a decent result was so painful that there was this sunk cost thing going on. I had the map now, it was pretty good, but no-one needed a timezone picker.

b8rjs, timezone-map.svg, timezones.json

The half-completed timezone picker I had written using b8rjs, and I saw this as an opportunity to write something using xinjs as a pure web-component. But I knew I had the map and a data file containing timezone metadata (the names of timezones, their offsets, etc.) which I had gotten from some github repo. That would be my starting point. How hard could it be?

Well, first of all the svg was about 400kB and the metadata was close to 100kB. Even compressed, this seemed like it would be a pretty big widget. I started off trying to make it lazy-load the data… I also wrote a function to process the svg and round all the point data to integers (the resolution of the file was 5000×3000 or so anyway). This got me closer to 300kB.

But then I started encountering all these bugs related to errors in the timezone metadata I remembered struggling with two years ago. Screw that.


I then discovered that you can interrogate Intl (a Javascript global) for a list of all accepted timezone names. A little more delving revealed you could trick Intl into telling you the offsets for all these timezones. So I could ditch my json file altogether and get rid of all the errors. That was over 80kB of data gone. A big win!

// @ts-ignore-error
export const timezones: Timezone[] = Intl.supportedValuesOf('timeZone').map(name => {
  // @ts-expect-error
  const offset = Number(Intl.DateTimeFormat('en-GB', {
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: 'shortOffset',
    timeZone: name
  }).format(timeNow).split('GMT').pop().replace(/\:30/, '.5'))

  const abbr = Intl.DateTimeFormat('en-GB', {
    hour: 'numeric',
    minute: 'numeric',
    timeZoneName: 'short',
    timeZone: name
  }).format(timeNow).split(' ').pop()

  return {

At this point I’ll give a shout-out to Vectornator as, arguably, the least terrible SVG editor I was able to lay my hands on—I also have Sketch, Graphic, Inkscape, Acorn (it actually has a very decent SVG editor), and Affinity Designer 2 in case you’re wondering. For some reason I started with Graphic. It hurts when I do that. I need to stop doing that. Affinity Designer 2 is too slow to launch. (Another Illustrator feature it has matched?)

With this new—presumably error-free—data to work with, I immediately discovered all kinds of problems with my SVG file. After finding and fixing several of the errors, I discovered that the shape covering the UK wasn’t attached to UTC. I soon discovered huge numbers of similar errors and gave up. Time to find a new SVG (or draw one myself…)

I’d performed this search before, and it wasn’t any better this time. Then I found a pretty darn nice graphical timezone picker written using jQuery. My first thought was “can I steal the SVG”? Turns out the guy was generating the SVG programmatically from strings of integer point data. I am liking this guy more by the minute.

Long story short, I grabbed his data and turned it into regions.ts and then used it to generate my SVG on-the-fly (learning along the way that apparently cloneNode() does not work on name-spaced elements).

const regionKey = Symbol('region')

const timezoneMap = (): any => {
  const svg = document.createElementNS(SVG_XMLNS, 'svg')
  svg.setAttribute('viewBox', '0 0 500 250')
  svg.setAttribute('alt', 'world timezone map')
  svg.append( => {
      const polygon = document.createElementNS(SVG_XMLNS, 'polygon')
      // of course svg elements don't support the title attribute    
      const title = document.createElementNS(SVG_XMLNS, 'title')
      title.textContent = regionId(region)
      polygon.setAttribute('points', region.points)
      polygon[regionKey] = region
      return polygon
  return svg

Note the use of a symbol to hook metadata to the DOM. It’s one of my new favorite things (of course you need to fight Typescript constantly to do it).

Using the regions data instead of the SVG file took my uncompressed widget down from 320kB or so to 180kB. Another big win.

And now it was working beautifully—beautifully enough that I started playing with it. A lot. And started finding more and more errors. Timezones suck. Let’s all switch to GMT.

Inevitably, there were errors in the region data. (The worst involved China.) Some of the timezone names were either wrong or had changed or… committee entropy or something. E.g. America/Argentina/Buenos_Aires is America/Buenos_Aires according to Chrome and Safari (but not Firefox). Anyway, most of that was fairly easily fixed by writing some code that looked for discrepancies between the browser-data from Intl and the region data. So most of that was dealt with fairly quickly—but not the Buenos Aires issue…

Worse, a lot of islands were represented by degenerate polygons (e.g. typically only two points) with a “pin point”. Also when I tried to cover up gaps between the polygons by adding a modest stroke all these degenerate polygons really ruined the whole map. And I also noticed that there was a crazy amount of detail in some parts of the map (e.g. Greenland) that weren’t helping anyone. What to do?


It occurred to me that it would be really useful to be able to compute the area of a polygon. E.g. I could measure the size of regions and figure out what to do with the tiny ones. Similarly, I could use an area function to simplify polygons (if you look at three successive points on the border of a polygon and the area of the triangle they form is less than some threshold value, then you can eliminate the middle point).

So I wrote a small polygon library—of which I am inordinately proud—to do this, and it can simplify polygons and compute their areas. I even wrote tests!

Aside; i just published the polygons library separately.

I decided to replace the degenerate polygons with small squares centered on the pin positions of the timezones in the metadata which seemed to be roughly in the right places, and then I simplified the polygons using higher and higher thresholds until things started looking funny and then pulled back and got the uncompressed size of the widget down to under 100kB.

The missing part of Papua New Guinea…

Finally, two timezones, corresponding to two of the largest islands in the world (Papua New Guinea and Madagascar) started bugging me (both were degenerate polygons in the original data). So I drew them in a vector program and discovered that I needed to convert them from <path> data into polygon point data, which was pretty straightforward (once I found a different tool that let me remove all the bezier control handles I didn’t create from the paths).

End result? Certainly it’s impressively small!

  • xinjs-timezone-picker bundle size is 98.51KB -> 30.91KB (gzip)
  • timezone-picker is bundle size is 4.32MB -> 902.53KB (gzip)

It also responsive, passes Safari’s accessibility audit (Chrome’s has been crashing on me and producing garbage results for months now), and it’s mobile-friendly (I haven’t discussed how you can also enter your precise timezone via auto-complete (using <input list="..."> and <datalist>) because implementing that part was literally five minutes’ work.

Finally <timezone-picker> automatically defaults to your current timezone and its value is guaranteed to be a timezone supported by your browser.

Now if only I had a earthly use for it…

Post Script

I recently came back to this post to discover my timezone picker was broken. WTF? Turns out that at some point it was decided America/Montreal should be merged into America/Toronto (part of the international conspiracy to annoy Francophones, I guess) and at some point recently Chrome decided to stop supporting it which meant that timezone-picker couldn’t find timezone offset information for the region around Montreal. Oops.

Anyway, it’s fixed in 0.4.3.