Display an event across time zones in JavaScript

Build an event-time display with a UTC instant, destination time zones, and a clear local date, time, and offset for each place.

JavaScript (browser)Last tested Get the complete example ↓
On this page

Working example

HTML + JavaScript

Before you start

Build a small time display for a webinar, livestream, or distributed team event. Enter its UTC date and time, choose three places, and show the local date, time, and UTC offset in each one. A Tuesday event can happen on Wednesday for someone else; the result makes that visible.

The example calls the Timezone API once per distinct destination when the form is submitted. It uses one exact event instant. It does not infer a time zone from the visitor's browser or resolve a local time such as "1:30 AM in New York."

Create a Public key on the keys page. Allow localhost and your site's hostname. Replace YOUR_PUBLIC_API_KEY in the downloaded HTML, then serve it from its folder:

Terminal
python3 -m http.server 8000

Open the local example. Use the allowed hostname instead of opening the file directly. Keep secret keys on your server.

1. Start with one explicit instant

Label both inputs as UTC. A time input has no time-zone information of its own, so the form's contract must supply it.

HTML
<form id="event-form" aria-busy="false">
  <div class="fields">
    <label>Event date in UTC<input id="event-date" type="date" min="2000-01-01" max="2099-12-31" value="2026-09-15" aria-describedby="utc-hint" required></label>
    <label>Event time in UTC<input id="event-time" type="time" step="60" value="16:00" aria-describedby="utc-hint" required></label>
  </div>
  <p id="utc-hint" class="hint">Enter a UTC time, not your computer's local time. Results use a 24-hour clock.</p>
  <fieldset>
    <legend>Display in these places</legend>
    <div class="destinations">
      <label>Place 1<select id="zone-one" class="zone"></select></label>
      <label>Place 2<select id="zone-two" class="zone"></select></label>
      <label>Place 3<select id="zone-three" class="zone"></select></label>
    </div>
  </fieldset>
  <button type="submit">Show event times</button>
  <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">No requests until you ask.</p>
</form>

Join the validated date and time into 2026-09-15T16:00:00Z. The final Z explicitly identifies UTC. The function also checks the calendar date so an impossible day cannot silently roll into the next month.

JavaScript
function eventInstant(date, time) {
  if (!/^20\d{2}-\d{2}-\d{2}$/.test(date) || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(time)) {
    throw new Error('Enter a UTC date between 2000 and 2099 and a time in HH:MM format.');
  }
  const instant = `${date}T${time}:00Z`;
  const ms = Date.parse(instant);
  if (!Number.isFinite(ms) || new Date(ms).toISOString() !== instant.replace('Z', '.000Z')) {
    throw new Error('Enter a valid calendar date.');
  }
  return instant;
}

Do not pass these fields through a constructor that interprets them in the browser's local zone. JavaScript dates represent instants, while many display methods apply the computer's local time zone. MDN's Date reference explains that distinction.

This example accepts dates from 2000 through 2099 at minute precision. If your event is already stored as an offset-bearing timestamp, normalize that known instant to UTC before filling the form. If you only have a local wall time, first resolve it with an explicit policy for repeated or nonexistent times around daylight-saving changes.

2. Ask for each destination at that instant

Call /timezone/UTC?to={timezone}&at={time} with the same UTC timestamp for every destination. Use IANA IDs such as America/New_York, not an abbreviation such as EST or a fixed offset such as -05:00.

JavaScript
async function convertEvent(zone, instant, signal) {
  if (!API_KEY.startsWith('parse_public_')) throw new Error('Add your public API key and allow this hostname.');
  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) {
    const messages = {
      400: 'The event time could not be read. Check the date and time.',
      401: 'Check your public API key.',
      403: 'Check your public key and allowed hostnames.',
      404: 'A selected time zone is unavailable.',
      429: 'The request limit was reached. Try again later.'
    };
    throw new Error(messages[response.status] || 'Event times are unavailable. Try again later.');
  }
  return readConversion(await response.json(), zone, instant);
}

function readConversion(data, zone, instant) {
  const fail = () => new Error('Could not verify the converted time. Try again.');
  const target = data?.to;
  if (!ZONES[zone] || data?.timezone !== 'UTC' || data.at !== instant.replace('Z', '+00:00')
    || !target || ![zone, ...(ZONES[zone].aliases || [])].includes(target.timezone)
    || !(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, timezone: target.timezone, at: target.at, date, time: time.slice(0, 5), weekday,
    offset: target.offset, abbreviation: target.abbreviation };
}

The converted answer is in to. Its at field includes the destination's local date, time, and offset. offset_minutes gives the signed numeric offset, and abbreviation may be null. A missing abbreviation does not prevent the example from displaying a verified time and numeric offset.

The example checks that the response belongs to the selected zone and that the returned timestamp represents the requested instant. It also checks that the timestamp's offset agrees with offset and offset_minutes. An incomplete or inconsistent result stops the display instead of guessing.

The API can return an equivalent canonical zone ID. For the included Kolkata option, the example accepts both Asia/Kolkata and Asia/Calcutta. Add equivalent IDs deliberately when expanding the menu; do not accept an arbitrary response zone.

3. Keep the destination's date visible

Render the date and clock components returned by the API. Calculate the weekday from that local calendar date using UTC only as a formatting convention. This keeps the visitor's own time zone from changing the displayed result a second time.

JavaScript
function showTimes(rows, instant) {
  document.querySelector('#instant').textContent = `One instant: ${instant}. All times use a 24-hour clock.`;
  document.querySelector('#times').replaceChildren(...rows.map(row => {
    const item = document.createElement('li');
    const place = document.createElement('div');
    const name = document.createElement('h2'); name.textContent = row.label;
    const zone = document.createElement('p'); zone.className = 'zone-id'; zone.textContent = row.timezone;
    place.append(name, zone);
    const when = document.createElement('div'); when.className = 'when';
    const time = document.createElement('time'); time.setAttribute('datetime', row.at);
    const date = document.createElement('span'); date.className = 'date'; date.textContent = `${row.weekday}, ${row.date}`;
    const clock = document.createElement('strong'); clock.className = 'clock'; clock.textContent = row.time;
    time.append(date, clock);
    const offset = document.createElement('p'); offset.className = 'offset';
    offset.textContent = `UTC${row.offset}${row.abbreviation ? ` · ${row.abbreviation}` : ''}`;
    when.append(time, offset);
    item.append(place, when);
    return item;
  }));
  result.hidden = false;
  setStatus('Event times ready. Check each date as well as the time.');
}

Each row shows the chosen place, returned IANA zone, weekday, full date, 24-hour time, and UTC offset. The offset is useful even when an abbreviation is unavailable or unfamiliar. Text from the response is inserted with textContent.

Avoid calling new Date(to.at).toLocaleString() without an explicit destination time zone: that normally formats the instant in the visitor's zone. The example keeps the API's converted clock components intact.

4. Finish one calculation before showing it

The three destination requests run together. Duplicate choices make one request and produce one row. Results appear only when every selected destination has passed validation; a failed request leaves the result hidden and the form available for another attempt.

Nothing is requested on page load. Editing the date, time, or a destination cancels pending requests and clears the old display. A revision check rejects late responses for old inputs, and a timeout prevents a slow request from leaving the form busy indefinitely.

Try midnight and daylight-saving boundaries

These checks exercise the parts that fixed offsets often miss:

  • Tokyo, next day: 2026-09-15T16:00:00Z becomes Wednesday, September 16 at 01:00, with offset +09:00.
  • Kolkata, half-hour offset: the same instant becomes Tuesday, September 15 at 21:30, with offset +05:30.
  • Los Angeles, previous year: 2026-01-01T00:30:00Z becomes Wednesday, December 31, 2025 at 16:30, with offset -08:00.
  • New York, before the clock change: 2026-03-08T06:30:00Z becomes Sunday, March 8 at 01:30, with offset -05:00.
  • New York, after the clock change: 2026-03-08T07:30:00Z becomes Sunday, March 8 at 03:30, with offset -04:00.

The last two examples are one hour apart as instants. The local clock jumps from 01:30 to 03:30. Recalculate using the actual event date; today's offset is not a safe substitute for the event's offset.

Use it on an event page

Store the UTC instant as the event's source of truth and keep the destination zone IDs with the display settings. You can replace the editable inputs with your event record when adapting this example. The current form sends the instant and selected zone IDs to parseAPI; it does not create an event or send invitations.

A recurring meeting is a separate scheduling task. "Every Monday at 9 AM in New York" needs a local recurrence rule and zone, because its UTC time can change across the year. This tutorial displays one occurrence at a time. Future time-zone rules can also change, so refresh conversions when showing an upcoming event.

See the Timezone reference for the full response and other lookup options.

Complete example

One HTML file with the form, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.

Download HTML
View and copy the complete source
event-time-zones.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Display an event across time zones</title>
  <style>
    :root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
    @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; } }
    :root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    :root[data-theme="light"] { color-scheme: light; }
    * { box-sizing: border-box; }
    [hidden] { display: none !important; }
    body { margin: 0; padding: 24px; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
    main { max-width: 560px; margin: 0 auto; }
    h1 { margin: 0 0 6px; font-size: clamp(23px, 5vw, 30px); font-weight: 600; letter-spacing: -.035em; }
    p { margin: 0; }
    .intro, .hint, .note { color: var(--muted); }
    form { margin-top: 24px; }
    .fields { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }
    label { display: block; font-size: 13px; font-weight: 550; }
    input, select, button { font: inherit; }
    input, select { width: 100%; min-width: 0; margin-top: 7px; min-height: 46px; padding: 11px 10px; border: 1px solid var(--line); border-radius: 7px; background: var(--field); color: var(--ink); }
    .hint { margin: 8px 0 16px; font-size: 12px; }
    fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
    legend { padding: 0; margin-bottom: 8px; font-size: 13px; font-weight: 550; }
    .destinations { display: grid; gap: 12px; }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
    button { min-height: 46px; margin-top: 16px; padding: 11px 18px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
    button:disabled { opacity: .55; cursor: wait; }
    #status { min-height: 44px; margin-top: 14px; color: var(--muted); font-size: 13px; }
    #status[data-error="true"] { color: var(--error); }
    #instant { margin: 4px 0 16px; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
    #times { list-style: none; padding: 0; margin: 0; }
    #times li { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; padding: 18px 0; border-top: 1px solid var(--line); }
    h2 { margin: 0; font-size: 16px; font-weight: 600; }
    .zone-id, .offset { color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
    .when { text-align: right; }
    .date { display: block; font-size: 13px; }
    .clock { display: block; font-size: 32px; font-weight: 600; color: var(--accent); letter-spacing: -.04em; }
    .note { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 12px; }
    @media (max-width: 380px) { body { padding: 18px; } .fields { grid-template-columns: minmax(0, 1fr); } button { width: 100%; } #times li { grid-template-columns: minmax(0, 1fr); gap: 8px; } .when { text-align: left; } }
  </style>
</head>
<body>
  <main>
    <h1>One event. Every time zone.</h1>
    <p class="intro">Show the same event in three places, with the local date and UTC offset beside each time.</p>
    <!-- example:form -->
    <form id="event-form" aria-busy="false">
      <div class="fields">
        <label>Event date in UTC<input id="event-date" type="date" min="2000-01-01" max="2099-12-31" value="2026-09-15" aria-describedby="utc-hint" required></label>
        <label>Event time in UTC<input id="event-time" type="time" step="60" value="16:00" aria-describedby="utc-hint" required></label>
      </div>
      <p id="utc-hint" class="hint">Enter a UTC time, not your computer's local time. Results use a 24-hour clock.</p>
      <fieldset>
        <legend>Display in these places</legend>
        <div class="destinations">
          <label>Place 1<select id="zone-one" class="zone"></select></label>
          <label>Place 2<select id="zone-two" class="zone"></select></label>
          <label>Place 3<select id="zone-three" class="zone"></select></label>
        </div>
      </fieldset>
      <button type="submit">Show event times</button>
      <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">No requests until you ask.</p>
    </form>
    <!-- /example:form -->
    <section id="result" aria-label="Event times" hidden>
      <p id="instant"></p>
      <ul id="times"></ul>
    </section>
    <p class="note">This is one event instant. A recurring meeting needs its own schedule and time-zone policy.</p>
  </main>
  <script>
    const API_KEY = 'YOUR_PUBLIC_API_KEY';
    const ZONES = {
      'America/Los_Angeles': { label: 'Los Angeles' },
      'America/New_York': { label: 'New York' },
      'Europe/London': { label: 'London' },
      'Europe/Paris': { label: 'Paris' },
      'Asia/Kolkata': { label: 'Kolkata', aliases: ['Asia/Calcutta'] },
      'Asia/Tokyo': { label: 'Tokyo' },
      'Australia/Sydney': { label: 'Sydney' }
    };
    const form = document.querySelector('#event-form');
    const button = form.querySelector('button');
    const status = document.querySelector('#status');
    const result = document.querySelector('#result');
    const selects = [...document.querySelectorAll('.zone')];
    const defaults = ['America/New_York', 'Europe/London', 'Asia/Tokyo'];
    selects.forEach((select, index) => {
      for (const [id, zone] of Object.entries(ZONES)) {
        const option = document.createElement('option');
        option.value = id;
        option.textContent = zone.label;
        select.append(option);
      }
      select.value = defaults[index];
    });
    let controller;
    let revision = 0;

    // example:instant
    function eventInstant(date, time) {
      if (!/^20\d{2}-\d{2}-\d{2}$/.test(date) || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(time)) {
        throw new Error('Enter a UTC date between 2000 and 2099 and a time in HH:MM format.');
      }
      const instant = `${date}T${time}:00Z`;
      const ms = Date.parse(instant);
      if (!Number.isFinite(ms) || new Date(ms).toISOString() !== instant.replace('Z', '.000Z')) {
        throw new Error('Enter a valid calendar date.');
      }
      return instant;
    }
    // /example:instant

    // example:request
    async function convertEvent(zone, instant, signal) {
      if (!API_KEY.startsWith('parse_public_')) throw new Error('Add your public API key and allow this hostname.');
      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) {
        const messages = {
          400: 'The event time could not be read. Check the date and time.',
          401: 'Check your public API key.',
          403: 'Check your public key and allowed hostnames.',
          404: 'A selected time zone is unavailable.',
          429: 'The request limit was reached. Try again later.'
        };
        throw new Error(messages[response.status] || 'Event times are unavailable. Try again later.');
      }
      return readConversion(await response.json(), zone, instant);
    }

    function readConversion(data, zone, instant) {
      const fail = () => new Error('Could not verify the converted time. Try again.');
      const target = data?.to;
      if (!ZONES[zone] || data?.timezone !== 'UTC' || data.at !== instant.replace('Z', '+00:00')
        || !target || ![zone, ...(ZONES[zone].aliases || [])].includes(target.timezone)
        || !(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, timezone: target.timezone, at: target.at, date, time: time.slice(0, 5), weekday,
        offset: target.offset, abbreviation: target.abbreviation };
    }
    // /example:request

    function setStatus(message, error = false) { status.textContent = message; status.dataset.error = String(error); }
    function cancel() {
      revision += 1;
      controller?.abort();
      controller = undefined;
      result.hidden = true;
      document.querySelector('#times').replaceChildren();
      button.disabled = false;
      form.setAttribute('aria-busy', 'false');
    }

    // example:result
    function showTimes(rows, instant) {
      document.querySelector('#instant').textContent = `One instant: ${instant}. All times use a 24-hour clock.`;
      document.querySelector('#times').replaceChildren(...rows.map(row => {
        const item = document.createElement('li');
        const place = document.createElement('div');
        const name = document.createElement('h2'); name.textContent = row.label;
        const zone = document.createElement('p'); zone.className = 'zone-id'; zone.textContent = row.timezone;
        place.append(name, zone);
        const when = document.createElement('div'); when.className = 'when';
        const time = document.createElement('time'); time.setAttribute('datetime', row.at);
        const date = document.createElement('span'); date.className = 'date'; date.textContent = `${row.weekday}, ${row.date}`;
        const clock = document.createElement('strong'); clock.className = 'clock'; clock.textContent = row.time;
        time.append(date, clock);
        const offset = document.createElement('p'); offset.className = 'offset';
        offset.textContent = `UTC${row.offset}${row.abbreviation ? ` · ${row.abbreviation}` : ''}`;
        when.append(time, offset);
        item.append(place, when);
        return item;
      }));
      result.hidden = false;
      setStatus('Event times ready. Check each date as well as the time.');
    }
    // /example:result

    form.addEventListener('input', () => { cancel(); setStatus('Inputs changed. Show event times again.'); });
    form.addEventListener('submit', async event => {
      event.preventDefault();
      cancel();
      if (!form.reportValidity()) return;
      const current = revision;
      const request = new AbortController();
      controller = request;
      let timedOut = false;
      const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 10000);
      try {
        const instant = eventInstant(document.querySelector('#event-date').value, document.querySelector('#event-time').value);
        const zones = [...new Set(selects.map(select => select.value))];
        if (zones.some(zone => !Object.hasOwn(ZONES, zone))) throw new Error('Choose a supported time zone.');
        button.disabled = true;
        form.setAttribute('aria-busy', 'true');
        setStatus('Checking event times...');
        const rows = await Promise.all(zones.map(zone => convertEvent(zone, instant, request.signal)));
        if (current !== revision) return;
        if (request.signal.aborted) throw new Error('The request was cancelled.');
        showTimes(rows, instant);
      } catch (error) {
        if (current !== revision) return;
        request.abort();
        setStatus(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 : 'Could not display event times. Try again.', true);
      } finally {
        clearTimeout(timeout);
        if (current === revision) { button.disabled = false; form.setAttribute('aria-busy', 'false'); controller = undefined; }
      }
    });
  </script>
</body>
</html>

Need a key? Create a public API key. For every field and parameter, see Timezone API reference.