Open-source project
vvo/tzdb avatar
vvo/tzdb

@vvo/tzdb: an IANA time zone list for pickers that is rebuilt every day

Always up to date IANA time zones for time zone pickers: grouped list with major cities, current offsets, DST abbreviations, and the country of any time zone. Updated daily.

916 stars61 forksTypeScriptMIT

At a glance

What is it?
@vvo/tzdb ships every IANA time zone grouped by country and clock, with major cities, current and standard offsets, daylight saving abbreviations and the country of any zone, and it is regenerated by a daily workflow from the IANA database and GeoNames. The unusual part is that current offsets are computed in the browser at runtime rather than baked into the package, which means a site that installs it once still follows daylight saving changes.
Who is it for?
Adopt @vvo/tzdb when you build a time zone picker and want current offsets, daylight saving abbreviations and localised zone names without shipping a stale copy of the IANA database. Do not adopt it if you need historical offsets or transitions, since the package exposes current and standard offsets rather than a transition history.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Data updated daily, offsets computed in the browser

The headline feature of @vvo/tzdb is not the data itself but how it stays correct. Every day, a GitHub workflow compiles the latest IANA tz database, takes city populations from GeoNames, tests the result and publishes a new version when something changed.

A daily publish means the package version and the underlying data move together, so there is no quarterly refresh to schedule and no stale fork to maintain.

The offsets are handled differently, and this is the design decision that matters most. Current offsets come from the browser or Node.js at runtime. A site that never updates this package still follows daylight saving changes, and agrees with the offsets Intl.DateTimeFormat gives the rest of the app.

That is a real answer to a real problem. Bundled offset tables are the reason so many applications show the wrong time for half the year. If the offset is derived from the runtime's own time zone database, the picker cannot disagree with the rest of the application, and a package that was installed two years ago still behaves correctly in March.

The rest of the feature list is short and specific: every IANA time zone grouped by country and clock with major cities, current offsets and abbreviations such as PDT in summer and PST in winter, every current and deprecated IANA name so saved values always find their entry, names in any language the browser supports, and the country of a visitor's time zone without an IP lookup. There are no dependencies, both ESM and CommonJS are shipped, and TypeScript types are included.

What one time zone entry looks like

getTimeZones returns the list sorted by current offset, and each entry carries a fairly wide set of fields.

js
{
  name: "America/Los_Angeles",
  alternativeName: "Pacific Time",
  group: ["America/Los_Angeles", "US/Pacific", "PST8PDT"],
  continentCode: "NA",
  continentName: "North America",
  countryName: "United States",
  countryCode: "US",
  mainCities: ["Los Angeles", "San Diego", "San Jose", "San Francisco"],
  rawOffsetInMinutes: -480,
  abbreviation: "PST",
  dstAbbreviation: "PDT",
  rawFormat: "-08:00 Pacific Time - Los Angeles, San Diego, San Jose, San Francisco",
  currentTimeOffsetInMinutes: -420,
  currentTimeFormat: "-07:00 Pacific Time - Los Angeles, San Diego, San Jose, San Francisco",
  currentAbbreviation: "PDT",
}

The two time formats are the ones a picker renders, offset plus name plus cities, and the pair of them is what makes daylight saving visible: the raw format is the standard offset and the current format is the offset in effect now, which is why the example shows minus eight and minus seven for the same zone.

The group array is the compatibility layer. It holds every IANA name that maps to the same clock, so America/Los_Angeles, US/Pacific and PST8PDT all resolve to the same entry. That is why a saved value such as Asia/Calcutta, a name that browsers still return for Kolkata, still finds its row.

The abbreviations come from IANA, and the dstAbbreviation field is only present for zones that have daylight saving time.

Four options, and two behaviours that catch people out

getTimeZones takes four options, and each one answers a question a picker eventually asks.

The locale option translates alternativeName, countryName and continentName, so with the French locale Los Angeles shows as a French zone name while the city names stay as they are. The includeUtc option adds Etc/UTC, whose group includes both UTC and GMT. The date option computes the current fields at a given date rather than now, which is the option you need when the picker is choosing a time zone for a future meeting rather than for right now. The includeGmtOffsets option adds the fixed offsets from Etc/GMT-14 to Etc/GMT+12, and the README warns that as in POSIX the sign is inverted, so Etc/GMT+5 is five hours behind UTC.

Two runtime behaviours are worth reading twice.

Zones the browser does not know yet are left out, since Intl could not format dates in them. That is the price of computing offsets at runtime, and it is a price worth paying, but it means the list you get depends on the browser.

Runtimes without Intl time zone support get every zone at its standard offset. So the fallback is well defined rather than broken, but a server side render in an old runtime will show standard offsets where a browser shows the current one.

The country of a visitor, without an IP lookup

The second export is getCountryCode, and it exists because a picker often needs to know where a visitor is without doing a geo lookup.

js
import { getCountryCode } from "@vvo/tzdb";

const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; // "Europe/Paris"
getCountryCode(timeZone); // "FR"
new Intl.DisplayNames(["en"], { type: "region" }).of("FR"); // "France"

It returns the ISO 3166 country code of an IANA time zone name, or undefined for names without a country such as UTC.

Two details make it practical. It knows every IANA name, including the old ones browsers still return, so a visitor whose browser reports Asia/Calcutta still resolves to a country code. And it weighs 4 kB and does not load the time zone list, which means you can call it on a page that never shows a picker.

That separation matters on a server, where loading a 17 kB list to answer one question about a single request would be wasteful. The package keeps the country lookup and the list as independent paths, and the README is explicit that the small one does not pull in the big one.

How zones are grouped, and why Edmonton is on its own

The grouping rule is unusual enough to be worth reading, because it explains the shape of the list.

Zones share an entry when they are in the same country and their clocks showed the same time for the past year and will for the next two. Detroit and New York had different rules a century ago, so IANA keeps both, but they share an entry today. Zones that just changed their rules, like Edmonton, keep their own entry for a year, while browsers catch up.

So the number of entries is not the number of IANA identifiers, and the count changes over time as rules converge or diverge. A UI that shows one row per IANA name would be both longer and more confusing, and this grouping gives one row per actual clock.

The alternativeName field follows the CLDR naming rules. It is the English name from CLDR with the words Standard, Daylight and Summer removed. When the summer and winter names differ, as with Greenwich Mean Time and British Summer Time, it uses the CLDR generic name, United Kingdom Time. And several countries share names like Eastern Time, which is why the README advises showing countryName or mainCities next to it in the picker.

That last point is a UI requirement rather than an option. A picker that renders only the zone name will show several identical rows.

The comparison table, and what it actually argues

The README compares @vvo/tzdb with the countries-and-timezones package, and the table is really an argument about who maintains the data.

Data updates are every day automatically against by hand. Current offsets come from the browser at runtime against being computed when the package was built. Abbreviations that follow daylight saving are supported against not supported. Translations work in any language the browser has against none. The country of a time zone costs 4 kB against 9 kB, and the picker list with cities and names costs 17 kB against not being available at all. The sizes given are minified and gzipped.

The trade-off underneath is maintenance capacity. A hand maintained data file is small and stable, but somebody has to update it, and until they do, the offsets are wrong for half the year. The daily workflow here means the data side is never stale, and the runtime computation means the offsets are never stale either.

The cost of that choice is size and a dependency on Intl. 17 kB gzipped for the picker list is fine for a page that shows a picker and wasteful for a page that does not, which is why the country lookup exists as a separate 4 kB entry point.

The package layout and the daily pipeline

The repository is a TypeScript project with a generate script, a test suite and a release workflow.

json
  "name": "@vvo/tzdb",
  "version": "0.0.0-development",
  "type": "module",
  "sideEffects": false,
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",

The version placeholder in the source manifest is replaced at release time, which is why the published versions look like v6.201.1, v6.201.0 and v6.200.0. The 6 is the major line, the 201 is the year, and the last number is a build counter, so a new release every day increments the last component.

The scripts show the toolchain in full. The build uses tsdown, formatting and linting use oxfmt and oxlint with type aware checking, plus tsc for types. The data is generated by node scripts/generate.ts. And the test script is worth noting, because it runs the build, then the node test runner over the test directory, then publint and attw against the packed tarball, which are the two tools that check that a published package actually resolves correctly for consumers.

The exports map publishes the main entry for both import and require with separate type files, and also exposes abbreviations.json, raw-time-zones.json and time-zones-names.json as subpath exports. Those are the same three data files that sit at the root of the repository, which means a consumer can grab the raw data without going through the API at all.

The other exports are rawTimeZones, the list without the current fields sorted by standard offset, timeZonesNames, every IANA name from zone.tab, abbreviations mapping long names to abbreviations, and tzdbVersion, which reports the IANA release behind the data such as 2026e. The abbreviations map carries a warning worth remembering, that CST means Central Standard Time at minus six, China Standard Time at plus eight and Cuba Standard Time at minus five.

Editorial conclusion

Adopt @vvo/tzdb when you build a time zone picker and want current offsets, daylight saving abbreviations and localised zone names without shipping a stale copy of the IANA database. Do not adopt it if you need historical offsets or transitions, since the package exposes current and standard offsets rather than a transition history. Before the first integration, decide how you will store the selected value, because the README recommends saving the IANA name such as Asia/Calcutta or US/Pacific rather than the offset, and check the two runtime caveats, since zones the browser does not know yet are left out of the list and a runtime without Intl time zone support sees every zone at its standard offset.

Frequently asked questions

How does @vvo/tzdb keep its data up to date?

Every day a GitHub workflow compiles the latest IANA tz database, takes city populations from GeoNames, tests the result and publishes a new version when something changed. Current offsets are then computed in the browser or in Node.js at runtime, so a site that never updates the package still follows daylight saving changes.

How do I build a time zone picker with @vvo/tzdb?

Install it with npm add @vvo/tzdb, call getTimeZones to get the list sorted by current offset, and render the currentTimeFormat of each entry as the option label. Match a saved value against the group array, which holds every current and deprecated IANA name for that clock, and save the name rather than the offset.

Can I find the country of a visitor without an IP lookup?

Yes, getCountryCode takes an IANA time zone name and returns the ISO 3166 country code, or undefined for names without a country such as UTC. It knows old names like Asia/Calcutta, weighs 4 kB and does not load the time zone list.

How does @vvo/tzdb handle localised zone names?

The locale option translates alternativeName, countryName and continentName, so with the French locale Los Angeles shows as a French zone name, while city names stay as they are. The zone name comes from CLDR with the words Standard, Daylight and Summer removed.

What happens if the browser does not know a time zone?

Zones the browser does not know yet are left out of the list, since Intl could not format dates in them. Runtimes without Intl time zone support get every zone at its standard offset, so the fallback is well defined but differs from what a browser would show.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. vvo/tzdb on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vvo-tzdb.svg)](https://hysenlabs.com/projects/vvo-tzdb)

Was this useful?