mezone,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).format(new Date());
}
// Usage:
getTimeInZone('America/New_York'); // "14:35:22"
getTimeInZone('Asia/Tokyo'); // "03:35:22"
getTimeInZone('Europe/London'); // "19:35:22"
```javascript
const timezones = Intl.supportedValuesOf('timeZone');
// Returns: ["Africa/Abidjan", "Africa/Accra", ..., "US/Pacific", ...]
console.log(timezones.length); // ~500+ timezones
const popularCities = [
{ label: 'New York', timezone: 'America/New_York' },
{ label: 'Los Angeles', timezone: 'America/Los_Angeles' },
{ label: 'Chicago', timezone: 'America/Chicago' },
{ label: 'London', timezone: 'Europe/London' },
{ label: 'Paris', timezone: 'Europe/Paris' },
{ label: 'Berlin', timezone: 'Europe/Berlin' },
{ label: 'Dubai', timezone: 'Asia/Dubai' },
{ label: 'Mumbai', timezone: 'Asia/Kolkata' },
{ label: 'Singapore', timezone: 'Asia/Singapore' },
{ label: 'Tokyo', timezone: 'Asia/Tokyo' },
{ label: 'Sydney', timezone: 'Australia/Sydney' },
{ label: 'SΓ£o Paulo', timezone: 'America/Sao_Paulo' },
];
2. Clock Component Architecture
class WorldClock {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.clocks = [];
this.interval = null;
}
addClock(label, timezone) {
this.clocks.push({ label, timezone });
this.render();
}
removeClock(timezone) {
this.clocks = this.clocks.filter(c => c.timezone !== timezone);
this.render();
}
formatTime(timezone) {
return new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(new Date());
}
formatDate(timezone) {
return new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
weekday: 'short',
month: 'short',
day: 'numeric'
}).format(new Date());
}
isNextDay(timezone) {
const localDate = new Date().toLocaleDateString();
const tzDate = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
month: '2-digit',
day: '2-digit',
year: 'numeric'
}).format(new Date());
return localDate !== tzDate;
}
render() {
if (!this.container) return;
this.container.innerHTML = this.clocks.map(({ label, timezone }) => {
const time = this.formatTime(timezone);
const date = this.formatDate(timezone);
const isDifferentDay = this.isNextDay(timezone);
return `
<div class="world-clock" data-timezone="${timezone}">
<div class="clock-city">${label}</div>
<div class="clock-time">${time}</div>
<div class="clock-date">${date}${isDifferentDay ? ' <span class="next-day">+1</span>' : ''}</div>
</div>
`;
}).join('');
}
start() {
this.render();
this.interval = setInterval(() => this.render(), 1000);
}
stop() {
if (this.interval) clearInterval(this.interval);
}
}
3. State Persistence & Initialization
const DEFAULT_CLOCKS = [
{ label: 'New York', timezone: 'America/New_York' },
{ label: 'London', timezone: 'Europe/London' },
{ label: 'Tokyo', timezone: 'Asia/Tokyo' },
];
async function loadClocks() {
const stored = await browser.storage.local.get('worldClocks');
return stored.worldClocks || DEFAULT_CLOCKS;
}
async function saveClocks(clocks) {
await browser.storage.local.set({ worldClocks: clocks });
}
// Initialize
const savedClocks = await loadClocks();
const worldClock = new WorldClock('clock-container');
savedClocks.forEach(c => worldClock.addClock(c.label, c.timezone));
worldClock.start();
.world-clocks-grid {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
}
.world-clock {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 12px 16px;
text-align: center;
min-width: 120px;
backdrop-filter: blur(10px);
}
.clock-city {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
opacity: 0.7;
margin-bottom: 4px;
}
.clock-time {
font-size: 24px;
font-weight: 600;
font-variant-numeric: tabular-nums; /* Prevents layout shift as numbers change */
}
.clock-date {
font-size: 11px;
opacity: 0.6;
margin-top: 2px;
}
.next-day {
color: #fbbf24;
font-size: 10px;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.world-clock {
background: rgba(0, 0, 0, 0.2);
}
}
5. Searchable Timezone Picker
function buildTimezonePicker(onSelect) {
const input = document.createElement('input');
input.placeholder = 'Search city...';
input.type = 'text';
const dropdown = document.createElement('ul');
dropdown.className = 'tz-dropdown hidden';
input.addEventListener('input', () => {
const query = input.value.toLowerCase();
const matches = popularCities.filter(c =>
c.label.toLowerCase().includes(query) ||
c.timezone.toLowerCase().includes(query)
).slice(0, 10);
dropdown.innerHTML = matches.map(c =>
`<li data-tz="${c.timezone}" data-label="${c.label}">${c.label}</li>`
).join('');
dropdown.classList.toggle('hidden', matches.length === 0);
});
dropdown.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) {
onSelect(li.dataset.label, li.dataset.tz);
input.value = '';
dropdown.classList.add('hidden');
}
});
return { input, dropdown };
}
Pitfall Guide
- Ignoring IANA IDs vs. Display Names:
Intl requires strict IANA timezone identifiers (e.g., America/New_York). Mapping user-friendly city names to these IDs requires a pre-defined lookup table; attempting to resolve city names dynamically will fail or return incorrect offsets.
- Layout Shift from Variable-Width Fonts: Omitting
font-variant-numeric: tabular-nums causes horizontal jitter as digits change width (e.g., 1 vs 8). This triggers continuous layout recalculations and degrades perceived performance.
- Unbounded
setInterval Re-renders: Calling render() every second without clearing previous intervals or implementing a stop/cleanup mechanism leads to memory leaks and overlapping timers, especially during hot-reloads or extension lifecycle transitions.
- Cross-Browser
Intl Feature Gaps: Older environments or restricted contexts (e.g., Web Workers, certain Node versions) may lack full Intl support. Always verify Intl.DateTimeFormat availability before instantiation, and provide fallback formatting for edge cases.
- Async Storage Race Conditions:
browser.storage.local operations are asynchronous. Initializing the clock UI before loadClocks() resolves causes a flash of default/empty state. Always await storage resolution before mounting components or implement a loading skeleton.
- DST Transition Blind Spots: Manual offset calculations or hardcoded hour differences ignore seasonal shifts. The
Intl API automatically applies current DST rules based on the host OS timezone database, but developers must ensure the extension doesn't cache formatted strings across DST boundaries without re-evaluation.
Deliverables
- π World Clock Architecture Blueprint: Component lifecycle diagram detailing state flow between
WorldClock class, browser.storage.local, and DOM rendering pipeline. Includes cleanup strategies for setInterval and extension background page synchronization.
- β
Pre-Launch Validation Checklist:
- Verify
Intl.supportedValuesOf('timeZone') availability in target Firefox versions
- Test DST transition behavior (spring forward/fall back) across 3+ regions
- Validate storage quota limits (
browser.storage.local.QUOTA_BYTES)
- Profile
setInterval render overhead using Firefox DevTools Performance tab
- Confirm
tabular-nums renders correctly across OS-level font stacks
- βοΈ Configuration Templates:
manifest.json storage permissions snippet
- Default timezone preset JSON schema
- CSS theme variable overrides for light/dark mode synchronization
- Extension event listeners for
browser.storage.onChanged to sync UI across tabs