Show an event in the visitor's time zone
Put a webinar on your site with the right local start time for each visitor.
On this page
Working example
HTML + JavaScriptMake the start time obvious
You have a webinar scheduled. Someone opens its page in New York, London, or Tokyo and immediately sees when it starts for them. They can change the time zone if they are planning to watch from somewhere else.
The example above loads automatically. Choose another time zone and watch the date change along with the time. Your visitors never need to enter the event's UTC timestamp.
Choose Use this example to put the complete HTML on your site.
1. Start with your saved event
Your application supplies the title and the scheduled instant:
const EVENT = {
title: 'Designing a better signup',
startsAt: '2026-10-15T18:00:00Z'
};Replace this sample with your event. The Z means UTC: one exact instant that everyone shares. If your scheduling system gives you a local time, resolve it to an instant using its time-zone rules before saving it.
2. Use the visitor's time zone
The browser supplies a useful starting choice:
function visitorZone() {
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return canonicalZone(zone) ? zone : null;
}Keep that choice editable. A traveler may be planning for another city, and a browser setting does not establish where someone lives. If the browser has no usable zone, the example asks them to choose one.
3. Convert the scheduled instant
Pass the event and the chosen zone to the Timezone API:
async function convertEvent(zone, instant, signal) {
const url = new URL('https://api.parseapi.com/timezone/UTC');
url.search = new URLSearchParams({to:zone, at:instant, key:API_KEY});
const response = await fetch(url, {signal});
if (!response.ok) throw new Error(response.status === 429
? 'The request limit was reached. Try again later.'
: [401,403].includes(response.status) ? 'Check your public key and allowed hostname.'
: 'The local time is unavailable. Try again.');
return readConversion(await response.json(), zone, instant);
}The to answer contains the local date, time, and offset at the event, including daylight-saving rules for that date. A lookup of today's offset would answer a different question.
4. Put the answer on the event page
function showTime(row) {
const date = new Date(`${row.date}T00:00:00Z`);
document.querySelector('#start').setAttribute('datetime', row.at);
document.querySelector('#clock').textContent = new Intl.DateTimeFormat('en', {
hour:'numeric', minute:'2-digit', timeZone:'UTC'
}).format(new Date(`${row.date}T${row.time}:00Z`));
document.querySelector('#date').textContent = new Intl.DateTimeFormat('en', {
weekday:'long', month:'long', day:'numeric', year:'numeric', timeZone:'UTC'
}).format(date);
document.querySelector('#offset').textContent = `${row.label} · UTC${row.offset}`;
}The visible date matters as much as the clock. This sample starts on Thursday afternoon in New York and Friday morning in Tokyo. The browser formats the returned clock components without converting them into a different time zone again.
Change the saved timestamp to an event near a clock change and try both cities. The complete download cancels old requests when the selection changes, verifies the returned instant, and keeps an unavailable time visible with a retry action.
This displays one scheduled occurrence. For a recurring series, calculate each occurrence from its schedule. To find a shared working window, continue with shared working hours.
Complete example
One HTML file with the markup, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.
View and copy the complete source
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Show an event in the visitor's time zone</title>
<style>
:root { color-scheme: light dark; --bg:#fff; --ink:#152029; --muted:#5e6b75; --line:#d9e2e7; --field:#f6f9fa; --accent:#007c91; --error:#b42335; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg:#0b1015; --ink:#eff7fa; --muted:#94a6b2; --line:#2c3942; --field:#121b23; --accent:#67e8f9; --error:#fda4af; } }
:root[data-theme="dark"] { color-scheme:dark; --bg:#0b1015; --ink:#eff7fa; --muted:#94a6b2; --line:#2c3942; --field:#121b23; --accent:#67e8f9; --error:#fda4af; }
:root[data-theme="light"] { color-scheme:light; }
* { box-sizing:border-box; }
[hidden] { display:none !important; }
body { margin:0; padding:28px; background:var(--bg); color:var(--ink); font:15px/1.5 system-ui,sans-serif; }
main { max-width:520px; margin:auto; }
p { margin:0; }
.eyebrow { color:var(--accent); font-size:11px; letter-spacing:.08em; text-transform:uppercase; }
h1 { margin:12px 0 8px; font-size:clamp(25px,6vw,34px); font-weight:600; letter-spacing:-.035em; line-height:1.15; }
.intro { color:var(--muted); max-width:34ch; }
.schedule { margin:28px 0 24px; border-block:1px solid var(--line); padding:24px 0; }
.label { font-size:13px; color:var(--muted); }
#clock { display:block; font-size:clamp(40px,10vw,58px); line-height:1.15; font-weight:600; letter-spacing:-.05em; }
#date { display:block; margin-top:5px; font-size:16px; }
#offset { margin-top:8px; color:var(--muted); font-size:12px; }
label { display:block; font-size:12px; color:var(--muted); }
select { display:block; width:100%; min-width:0; min-height:44px; margin-top:7px; padding:10px 40px 10px 12px; border:1px solid var(--line); border-radius:7px; background:var(--field); color:var(--ink); font:inherit; }
select:not([multiple]):where(:not([size]), [size="0"], [size="1"]) { -webkit-appearance:none; appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m4 6 4 4 4-4' fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 12px center; background-size:16px; }
#status { margin-top:12px; color:var(--muted); font-size:12px; }
#status[data-error="true"] { color:var(--error); }
button { margin-top:12px; min-height:44px; padding:0; border:0; background:none; color:var(--accent); font:inherit; cursor:pointer; }
:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
@media(max-width:380px) { body { padding:20px; } }
</style>
</head>
<body>
<main aria-busy="true">
<p class="eyebrow">Sample webinar · 45 minutes</p>
<h1 id="event-title">Designing a better signup</h1>
<p class="intro">A practical session on making the first few minutes feel effortless.</p>
<!-- example:form -->
<section class="schedule" aria-label="Event start time" aria-live="polite" aria-atomic="true">
<p class="label">Starts at</p>
<time id="start"><strong id="clock">—</strong><span id="date">Loading your local time...</span></time>
<p id="offset"></p>
</section>
<label>Time zone<select id="zone" aria-label="Time zone"></select></label>
<p id="status" role="status" aria-live="polite" data-error="false"></p>
<button id="retry" type="button" hidden>Try again</button>
<!-- /example:form -->
</main>
<script>
const API_KEY = 'YOUR_PUBLIC_API_KEY';
// example:instant
const EVENT = {
title: 'Designing a better signup',
startsAt: '2026-10-15T18:00:00Z'
};
// /example:instant
const ZONES = {
'America/Los_Angeles': { label:'Los Angeles' },
'America/Denver': { label:'Denver' },
'America/New_York': { label:'New York' },
'Europe/London': { label:'London' },
'Europe/Paris': { label:'Paris' },
'Asia/Kolkata': { label:'Kolkata' },
'Asia/Tokyo': { label:'Tokyo' },
'Australia/Sydney': { label:'Sydney' }
};
const main = document.querySelector('main');
const select = document.querySelector('#zone');
const status = document.querySelector('#status');
const retry = document.querySelector('#retry');
document.querySelector('#event-title').textContent = EVENT.title;
let controller;
let revision = 0;
function canonicalZone(zone) {
if (typeof zone !== 'string' || !zone.trim()) return null;
try { return new Intl.DateTimeFormat('en', {timeZone:zone}).resolvedOptions().timeZone; }
catch { return null; }
}
function eventInstant(value) {
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value)
|| !Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value.replace('Z', '.000Z')) {
throw new Error('Set the event to a valid UTC timestamp.');
}
return value;
}
// example:destination
function visitorZone() {
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return canonicalZone(zone) ? zone : null;
}
// /example:destination
// example:request
async function convertEvent(zone, instant, signal) {
const url = new URL('https://api.parseapi.com/timezone/UTC');
url.search = new URLSearchParams({to:zone, at:instant, key:API_KEY});
const response = await fetch(url, {signal});
if (!response.ok) throw new Error(response.status === 429
? 'The request limit was reached. Try again later.'
: [401,403].includes(response.status) ? 'Check your public key and allowed hostname.'
: 'The local time is unavailable. Try again.');
return readConversion(await response.json(), zone, instant);
}
// /example:request
function readConversion(data, zone, instant) {
const fail = () => new Error('Could not verify the converted time. Try again.');
const target = data?.to;
if (!canonicalZone(zone) || data?.timezone !== 'UTC' || data.at !== instant.replace('Z', '+00:00')
|| !target || canonicalZone(target.timezone) !== canonicalZone(zone)
|| !(target.abbreviation === null || typeof target.abbreviation === 'string' && target.abbreviation.trim())
|| typeof target.offset !== 'string' || !Number.isInteger(target.offset_minutes)
|| typeof target.at !== 'string') throw fail();
const match = target.at.match(/^(\d{4}-\d{2}-\d{2})T((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d)([+-])(\d{2}):([0-5]\d)$/);
if (!match) throw fail();
const [, date, time, sign, hours, minutes] = match;
const offset = (Number(hours) * 60 + Number(minutes)) * (sign === '-' ? -1 : 1);
const wall = `${date}T${time}`;
const wallMs = Date.parse(`${wall}Z`);
if (!Number.isFinite(wallMs) || new Date(wallMs).toISOString().slice(0, 19) !== wall
|| Math.abs(offset) > 840 || offset !== target.offset_minutes
|| target.offset !== `${sign}${hours}:${minutes}` || Date.parse(target.at) !== Date.parse(instant)) throw fail();
const weekday = new Intl.DateTimeFormat('en', { weekday: 'short', timeZone: 'UTC' }).format(new Date(wallMs));
return { label: ZONES[zone]?.label || zone.replaceAll('_', ' '), timezone: target.timezone, at: target.at, date, time: time.slice(0, 5), weekday,
offset: target.offset, abbreviation: target.abbreviation };
}
// example:result
function showTime(row) {
const date = new Date(`${row.date}T00:00:00Z`);
document.querySelector('#start').setAttribute('datetime', row.at);
document.querySelector('#clock').textContent = new Intl.DateTimeFormat('en', {
hour:'numeric', minute:'2-digit', timeZone:'UTC'
}).format(new Date(`${row.date}T${row.time}:00Z`));
document.querySelector('#date').textContent = new Intl.DateTimeFormat('en', {
weekday:'long', month:'long', day:'numeric', year:'numeric', timeZone:'UTC'
}).format(date);
document.querySelector('#offset').textContent = `${row.label} · UTC${row.offset}`;
}
// /example:result
function clearTime() {
document.querySelector('#start').removeAttribute('datetime');
document.querySelector('#clock').textContent = '—';
document.querySelector('#date').textContent = 'Local time unavailable';
document.querySelector('#offset').textContent = '';
}
async function refresh() {
revision += 1;
controller?.abort();
const current = revision;
const request = new AbortController();
controller = request;
clearTime();
retry.hidden = true;
status.dataset.error = 'false';
if (!canonicalZone(select.value)) {
status.textContent = 'Choose a time zone to see the start time.';
main.setAttribute('aria-busy', 'false');
return;
}
main.setAttribute('aria-busy', 'true');
document.querySelector('#date').textContent = 'Loading your local time...';
status.textContent = '';
let timedOut = false;
const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 10000);
try {
if (!API_KEY.startsWith('parse_public_')) throw new Error('Add your public API key and allow this hostname.');
const row = await convertEvent(select.value, eventInstant(EVENT.startsAt), request.signal);
if (current !== revision) return;
if (request.signal.aborted) throw new Error('The request was cancelled.');
showTime(row);
} catch (error) {
if (current !== revision) return;
clearTime();
status.dataset.error = 'true';
status.textContent = timedOut ? 'The request timed out. Try again.' : error instanceof TypeError
? 'Could not reach ParseAPI. Check your connection.' : error instanceof SyntaxError
? 'The response could not be read. Try again.' : error instanceof Error ? error.message : 'The local time is unavailable.';
retry.hidden = false;
} finally {
clearTimeout(timeout);
if (current === revision) { main.setAttribute('aria-busy', 'false'); controller = undefined; }
}
}
let detected;
try { detected = visitorZone(); } catch { detected = null; }
if (detected && !Object.hasOwn(ZONES, detected)) ZONES[detected] = {label:detected.replaceAll('_', ' ')};
const placeholder = document.createElement('option');
placeholder.value = ''; placeholder.textContent = 'Choose a time zone';
select.append(placeholder);
for (const [id, zone] of Object.entries(ZONES)) {
const option = document.createElement('option');
option.value = id; option.textContent = `${zone.label}${id === detected ? ' (your time zone)' : ''}`;
select.append(option);
}
select.value = detected || '';
select.addEventListener('change', refresh);
retry.addEventListener('click', refresh);
refresh();
</script>
</body>
</html>
Need a key? Set up this browser example. For every field and parameter, see Timezone API reference.