<tosijs-timezone-picker>

A graphical timezone picker inspired by Apple's, in about 30kB of geometry and no timezone dataset at all — the zones come from Intl at runtime.

Click a region, arrow-key around the map, or type into the field: the field autocompletes on both the zone name and the GMT offset, so Los and -7 both get you to America/Los_Angeles. value and timezone always hold a valid IANA timezone name.

<tosijs-timezone-picker timezone="Australia/Sydney"></tosijs-timezone-picker>
tosijs-timezone-picker {
  width: 100%;
  height: 100%;
}

Reading the value

The element fires change when the user picks a zone — from the map, the keyboard, or the text field.

<tosijs-timezone-picker id="picker"></tosijs-timezone-picker>
<div class="readout">—</div>
tosijs-timezone-picker {
  width: 100%;
  height: 100%;
}

.readout {
  font-family: monospace;
  padding: 10px 10px 40px;
}
const picker = preview.querySelector('#picker')
const readout = preview.querySelector('.readout')

const show = () => {
  const { name, abbr, offset } = picker.zone
  readout.textContent = `${name} — ${abbr} (GMT${offset > 0 ? '+' : ''}${offset || ''})`
}

picker.addEventListener('change', show)
show()

Attributes & properties

timezone attribute + property — the IANA name, e.g. 'Europe/Berlin'. Defaults to the local zone.
value property — kept in lock-step with timezone; setting either fires change.
zone read-only Timezone{ name, abbr, offset }.
region read-only Region — the map region backing the current zone, if any.

Assigning a name the runtime doesn't know is rejected: the element keeps the last zone the two properties agreed on and warns, rather than landing in a state its own render can't survive. "Doesn't know" is decided by Intl.DateTimeFormat, not by Intl.supportedValuesOf('timeZone') — engines list only one half of a renamed pair while accepting both, so a stored 'Europe/Kyiv' keeps working on an engine that only lists 'Europe/Kiev', and vice versa. See timezones.

Styling

Everything is a CSS custom property, so you can theme the picker from outside the shadow DOM. The element is 500×250 at --scale: 1; set width/height (or --scale) to resize it — the map keeps its aspect ratio inside whatever box you give it.

<tosijs-timezone-picker class="lava"></tosijs-timezone-picker>
.lava {
  --map-ocean: #1d1f2b;
  --map-land: #4a4458;
  --hover-color: #7a6a9a;
  --active-zone-color: #b4456f;
  --active-color: #ee257b;
  --font-color: white;
  --input-bg: #0006;
  --tooltip-bg: #ee257bee;
}
variable what it does
--scale scales the whole map (default 1 → 500×250); or set width/height directly
--map-ocean, --map-land base map colors
--hover-color, --hover-target-color the hovered offset band, and the region under the pointer
--active-zone-color, --active-color the selected offset band, and the selected region
--focus-color keyboard-focus ring on the map
--transition region color transition
--tooltip-bg, --tooltip-color, --tooltip-font-size the hover/keyboard tooltip
--inset, --padding, --input-bg, --input-radius position and shape of the text field
--font-size, --font-color, --font-family the text field's type

The map, tooltip, zoneName and liveRegion shadow parts are exposed for anything custom properties can't reach:

tosijs-timezone-picker::part(zoneName) { letter-spacing: 0.05em; }

Keyboard & assistive tech

The map is focusable. ←/→ step one GMT offset west/east (keeping roughly the same latitude), ↑/↓ move between zones within the current offset, north to south. Each move announces the zone through a visually-hidden aria-live region and shows the same tooltip the mouse does.

Consuming it as a blueprint

The component is written as a tosijs blueprint — a pure function that receives tosijs and returns a component class. That means it carries no copy of tosijs, and you choose the tag name, so two versions can coexist on one page:

<tosi-loader>
  <tosi-blueprint
    tag="my-timezone-picker"
    src="https://cdn.jsdelivr.net/npm/tosijs-timezone-picker/dist/blueprint.js"
  ></tosi-blueprint>
</tosi-loader>
<my-timezone-picker></my-timezone-picker>

…or, if you are already bundling:

import { makeComponent } from 'tosijs'
import blueprint from 'tosijs-timezone-picker/blueprint'

const { creator } = await makeComponent('my-timezone-picker', blueprint)
document.body.append(creator())

Importing the package normally does exactly this for you, with the tag <tosijs-timezone-picker>.