DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Handling Time Zones in Browser Extensions: Lessons from Building a World Clock

Handling Time Zones in Browser Extensions: Lessons from Building a World Clock

When I added world clock support to my Firefox new tab extension, I thought it would be simple — just use JavaScript's Intl API and call it a day. Turns out, there are several gotchas worth knowing about.

The JavaScript Intl.DateTimeFormat API

Modern browsers have excellent built-in time zone support:

function getTimeInZone(timezone) {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false
  }).format(new Date());
}

console.log(getTimeInZone('America/New_York'));   // 14:32:07
console.log(getTimeInZone('Europe/London'));       // 19:32:07
console.log(getTimeInZone('Asia/Tokyo'));          // 04:32:07
Enter fullscreen mode Exit fullscreen mode

No external libraries needed. No time zone database to maintain. Just a string identifier from the IANA time zone database.

The IANA Time Zone Database

IANA (Internet Assigned Numbers Authority) maintains the canonical list of time zone identifiers. These look like:

  • America/New_York
  • Europe/Paris
  • Asia/Kolkata
  • Pacific/Auckland
  • UTC

Avoid using abbreviations like EST or PST — they're ambiguous and not universally supported. EST could mean Eastern Standard Time (UTC-5) or Eastern Summer Time (UTC+11 in Australia).

Gotcha #1: DST Changes

Daylight Saving Time transitions are automatic with the Intl API — but you need to be aware they happen:

// This will automatically adjust for DST
const formatter = new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/Los_Angeles',
  hour: '2-digit',
  minute: '2-digit',
  hour12: false
});

// Winter: UTC-8
// Summer: UTC-7 (PDT)
// The formatter handles this automatically
Enter fullscreen mode Exit fullscreen mode

For display purposes, showing the UTC offset is more accurate than showing the abbreviation:

function getOffsetLabel(timezone) {
  const now = new Date();
  const formatter = new Intl.DateTimeFormat('en', {
    timeZone: timezone,
    timeZoneName: 'shortOffset'
  });
  const parts = formatter.formatToParts(now);
  return parts.find(p => p.type === 'timeZoneName')?.value || '';
}
// Returns "GMT-4", "GMT+5:30", etc.
Enter fullscreen mode Exit fullscreen mode

Gotcha #2: The Extension Runs in User's Local Time

Your extension background script runs in the user's system time zone. This is usually fine, but if you're doing any date math, be explicit:

// BAD: assumes local time
const hour = new Date().getHours(); // local hours

// GOOD: explicit about time zone
function getHourInZone(timezone) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    hour: 'numeric',
    hour12: false
  }).formatToParts(new Date());
  return parseInt(parts.find(p => p.type === 'hour').value);
}
Enter fullscreen mode Exit fullscreen mode

Gotcha #3: User Preferences for Time Format

Not everyone uses 24-hour time. Respect user preferences:

function formatTime(timezone, use24h = false) {
  return new Intl.DateTimeFormat('en-US', {
    timeZone: timezone,
    hour: '2-digit',
    minute: '2-digit',
    hour12: !use24h
  }).format(new Date());
}

// 12h: "02:32 PM"
// 24h: "14:32"
Enter fullscreen mode Exit fullscreen mode

Store this preference in browser.storage.sync so it follows the user across devices.

Live Clock Implementation

Here's a clean world clock implementation:

class WorldClock {
  constructor(container) {
    this.container = container;
    this.clocks = [];
    this.interval = null;
  }

  addClock(timezone, label) {
    this.clocks.push({ timezone, label });
    this.render();
  }

  removeClock(timezone) {
    this.clocks = this.clocks.filter(c => c.timezone !== timezone);
    this.render();
  }

  start() {
    this.render();
    this.interval = setInterval(() => this.render(), 1000);
  }

  stop() {
    if (this.interval) clearInterval(this.interval);
  }

  render() {
    const now = new Date();
    this.container.innerHTML = this.clocks.map(({ timezone, label }) => {
      const time = new Intl.DateTimeFormat('en-US', {
        timeZone: timezone,
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit',
        hour12: false
      }).format(now);

      const offset = new Intl.DateTimeFormat('en', {
        timeZone: timezone,
        timeZoneName: 'shortOffset'
      }).formatToParts(now).find(p => p.type === 'timeZoneName')?.value || '';

      return `
        <div class="clock-card">
          <div class="clock-label">${label}</div>
          <div class="clock-time">${time}</div>
          <div class="clock-offset">${offset}</div>
        </div>
      `;
    }).join('');
  }
}

// Usage
const clockContainer = document.getElementById('world-clocks');
const worldClock = new WorldClock(clockContainer);

browser.storage.sync.get('worldClocks').then(({ worldClocks = [] }) => {
  worldClocks.forEach(({ timezone, label }) => {
    worldClock.addClock(timezone, label);
  });
  worldClock.start();
});
Enter fullscreen mode Exit fullscreen mode

Performance: Don't Create a New Formatter Every Tick

Intl.DateTimeFormat instantiation is relatively expensive. Cache your formatters:

const formatterCache = new Map();

function getFormatter(timezone, options) {
  const key = `${timezone}:${JSON.stringify(options)}`;
  if (!formatterCache.has(key)) {
    formatterCache.set(key, new Intl.DateTimeFormat('en-US', {
      timeZone: timezone,
      ...options
    }));
  }
  return formatterCache.get(key);
}
Enter fullscreen mode Exit fullscreen mode

With 5 world clocks updating every second, this saves creating 5 new formatter objects per second.

Time Zone Picker UX

Let users search by city name, not just IANA identifier:

const POPULAR_ZONES = [
  { label: 'New York', timezone: 'America/New_York' },
  { label: 'Los Angeles', timezone: 'America/Los_Angeles' },
  { label: 'London', timezone: 'Europe/London' },
  { label: 'Paris', timezone: 'Europe/Paris' },
  { label: 'Tokyo', timezone: 'Asia/Tokyo' },
  { label: 'Sydney', timezone: 'Australia/Sydney' },
  { label: 'Dubai', timezone: 'Asia/Dubai' },
  { label: 'Mumbai', timezone: 'Asia/Kolkata' },
  { label: 'Singapore', timezone: 'Asia/Singapore' },
  { label: 'São Paulo', timezone: 'America/Sao_Paulo' },
];

function searchZones(query) {
  const q = query.toLowerCase();
  return POPULAR_ZONES.filter(z =>
    z.label.toLowerCase().includes(q) ||
    z.timezone.toLowerCase().includes(q)
  );
}
Enter fullscreen mode Exit fullscreen mode

What I Learned

For the Weather & Clock Dashboard Firefox extension, implementing world clocks taught me:

  1. Trust the platformIntl handles DST automatically
  2. Cache formatters — don't create new ones every second
  3. Use city labels — users think in cities, not IANA identifiers
  4. Respect 12/24h preference — store in browser.storage.sync
  5. Show UTC offset — more accurate than abbreviations like "EST"

The browser's built-in Intl API is genuinely excellent. No dependencies, accurate DST handling, and it stays current with time zone database updates via browser updates.


Weather & Clock Dashboard is a free, open-source Firefox new tab extension with live weather, world clocks, and a search bar. No tracking, MIT licensed.

Top comments (0)