All blocks
Timezone Picker
Select-style timezone picker with a search input inside the panel — IANA zones and live offsets from Intl, filtered accessibly.
Built withCombobox
MidwayGMT-11
NiueGMT-11
Pago PagoGMT-11
HonoluluGMT-10
RarotongaGMT-10
TahitiGMT-10
MarquesasGMT-9:30
AdakGMT-9
GambierGMT-9
AnchorageGMT-8
JuneauGMT-8
MetlakatlaGMT-8
NomeGMT-8
PitcairnGMT-8
SitkaGMT-8
YakutatGMT-8
CrestonGMT-7
DawsonGMT-7
Dawson CreekGMT-7
Fort NelsonGMT-7
HermosilloGMT-7
Los AngelesGMT-7
MazatlanGMT-7
PhoenixGMT-7
TijuanaGMT-7
VancouverGMT-7
WhitehorseGMT-7
Bahia BanderasGMT-6
BelizeGMT-6
BoiseGMT-6
Cambridge BayGMT-6
ChihuahuaGMT-6
Ciudad JuarezGMT-6
Costa RicaGMT-6
DenverGMT-6
EdmontonGMT-6
El SalvadorGMT-6
GalapagosGMT-6
GuatemalaGMT-6
InuvikGMT-6
ManaguaGMT-6
MeridaGMT-6
Mexico CityGMT-6
MonterreyGMT-6
ReginaGMT-6
Swift CurrentGMT-6
TegucigalpaGMT-6
BeulahGMT-5
BogotaGMT-5
CancunGMT-5
CaymanGMT-5
CenterGMT-5
ChicagoGMT-5
Coral HarbourGMT-5
EasterGMT-5
EirunepeGMT-5
GuayaquilGMT-5
JamaicaGMT-5
KnoxGMT-5
LimaGMT-5
MatamorosGMT-5
MenomineeGMT-5
New SalemGMT-5
OjinagaGMT-5
PanamaGMT-5
Rankin InletGMT-5
ResoluteGMT-5
Rio BrancoGMT-5
Tell CityGMT-5
WinnipegGMT-5
AnguillaGMT-4
AntiguaGMT-4
ArubaGMT-4
BarbadosGMT-4
Blanc-SablonGMT-4
Boa VistaGMT-4
Campo GrandeGMT-4
CaracasGMT-4
CuiabaGMT-4
CuracaoGMT-4
DetroitGMT-4
DominicaGMT-4
Grand TurkGMT-4
GrenadaGMT-4
GuadeloupeGMT-4
GuyanaGMT-4
HavanaGMT-4
IndianapolisGMT-4
IqaluitGMT-4
KralendijkGMT-4
La PazGMT-4
LouisvilleGMT-4
Lower PrincesGMT-4
ManausGMT-4
MarengoGMT-4
MarigotGMT-4
MartiniqueGMT-4
MonticelloGMT-4
MontserratGMT-4
NassauGMT-4
New YorkGMT-4
PetersburgGMT-4
Port of SpainGMT-4
Port-au-PrinceGMT-4
Porto VelhoGMT-4
Puerto RicoGMT-4
Santo DomingoGMT-4
St BarthelemyGMT-4
St KittsGMT-4
St LuciaGMT-4
St ThomasGMT-4
St VincentGMT-4
TorontoGMT-4
TortolaGMT-4
VevayGMT-4
VincennesGMT-4
WinamacGMT-4
AraguainaGMT-3
AsuncionGMT-3
BahiaGMT-3
BelemGMT-3
BermudaGMT-3
Buenos AiresGMT-3
CatamarcaGMT-3
CayenneGMT-3
CordobaGMT-3
CoyhaiqueGMT-3
FortalezaGMT-3
Glace BayGMT-3
Goose BayGMT-3
HalifaxGMT-3
JujuyGMT-3
La RiojaGMT-3
MaceioGMT-3
MendozaGMT-3
MonctonGMT-3
MontevideoGMT-3
PalmerGMT-3
ParamariboGMT-3
Punta ArenasGMT-3
RecifeGMT-3
Rio GallegosGMT-3
RotheraGMT-3
SaltaGMT-3
San JuanGMT-3
San LuisGMT-3
SantaremGMT-3
SantiagoGMT-3
Sao PauloGMT-3
StanleyGMT-3
ThuleGMT-3
TucumanGMT-3
UshuaiaGMT-3
St JohnsGMT-2:30
MiquelonGMT-2
NoronhaGMT-2
South GeorgiaGMT-2
Cape VerdeGMT-1
GodthabGMT-1
ScoresbysundGMT-1
AbidjanGMT+0
AccraGMT+0
AzoresGMT+0
BamakoGMT+0
BanjulGMT+0
BissauGMT+0
ConakryGMT+0
DakarGMT+0
DanmarkshavnGMT+0
FreetownGMT+0
LomeGMT+0
MonroviaGMT+0
NouakchottGMT+0
OuagadougouGMT+0
ReykjavikGMT+0
Sao TomeGMT+0
St HelenaGMT+0
AlgiersGMT+1
BanguiGMT+1
BrazzavilleGMT+1
CanaryGMT+1
CasablancaGMT+1
DoualaGMT+1
DublinGMT+1
El AaiunGMT+1
FaeroeGMT+1
GuernseyGMT+1
Isle of ManGMT+1
JerseyGMT+1
KinshasaGMT+1
LagosGMT+1
LibrevilleGMT+1
LisbonGMT+1
LondonGMT+1
LuandaGMT+1
MadeiraGMT+1
MalaboGMT+1
NdjamenaGMT+1
NiameyGMT+1
Porto-NovoGMT+1
TunisGMT+1
AmsterdamGMT+2
AndorraGMT+2
BelgradeGMT+2
BerlinGMT+2
BlantyreGMT+2
BratislavaGMT+2
BrusselsGMT+2
BudapestGMT+2
BujumburaGMT+2
BusingenGMT+2
CeutaGMT+2
CopenhagenGMT+2
GaboroneGMT+2
GibraltarGMT+2
HarareGMT+2
JohannesburgGMT+2
JubaGMT+2
KaliningradGMT+2
KhartoumGMT+2
KigaliGMT+2
LjubljanaGMT+2
LongyearbyenGMT+2
LubumbashiGMT+2
LusakaGMT+2
LuxembourgGMT+2
MadridGMT+2
MaltaGMT+2
MaputoGMT+2
MaseruGMT+2
MbabaneGMT+2
MonacoGMT+2
OsloGMT+2
ParisGMT+2
PodgoricaGMT+2
PragueGMT+2
RomeGMT+2
San MarinoGMT+2
SarajevoGMT+2
SkopjeGMT+2
StockholmGMT+2
TiraneGMT+2
TripoliGMT+2
TrollGMT+2
VaduzGMT+2
VaticanGMT+2
ViennaGMT+2
WarsawGMT+2
WindhoekGMT+2
ZagrebGMT+2
ZurichGMT+2
Addis AbabaGMT+3
AdenGMT+3
AmmanGMT+3
AntananarivoGMT+3
AsmeraGMT+3
AthensGMT+3
BaghdadGMT+3
BahrainGMT+3
BeirutGMT+3
BucharestGMT+3
CairoGMT+3
ChisinauGMT+3
ComoroGMT+3
DamascusGMT+3
Dar es SalaamGMT+3
DjiboutiGMT+3
FamagustaGMT+3
GazaGMT+3
HebronGMT+3
HelsinkiGMT+3
IstanbulGMT+3
JerusalemGMT+3
KampalaGMT+3
KievGMT+3
KirovGMT+3
KuwaitGMT+3
MariehamnGMT+3
MayotteGMT+3
MinskGMT+3
MogadishuGMT+3
MoscowGMT+3
NairobiGMT+3
NicosiaGMT+3
QatarGMT+3
RigaGMT+3
RiyadhGMT+3
SimferopolGMT+3
SofiaGMT+3
SyowaGMT+3
TallinnGMT+3
VilniusGMT+3
VolgogradGMT+3
TehranGMT+3:30
AstrakhanGMT+4
BakuGMT+4
DubaiGMT+4
MaheGMT+4
MauritiusGMT+4
MuscatGMT+4
ReunionGMT+4
SamaraGMT+4
SaratovGMT+4
TbilisiGMT+4
UlyanovskGMT+4
YerevanGMT+4
KabulGMT+4:30
AlmatyGMT+5
AqtauGMT+5
AqtobeGMT+5
AshgabatGMT+5
AtyrauGMT+5
DushanbeGMT+5
KarachiGMT+5
KerguelenGMT+5
MaldivesGMT+5
MawsonGMT+5
OralGMT+5
QostanayGMT+5
QyzylordaGMT+5
SamarkandGMT+5
TashkentGMT+5
VostokGMT+5
YekaterinburgGMT+5
CalcuttaGMT+5:30
ColomboGMT+5:30
KatmanduGMT+5:45
BishkekGMT+6
ChagosGMT+6
DhakaGMT+6
OmskGMT+6
ThimphuGMT+6
UrumqiGMT+6
CocosGMT+6:30
RangoonGMT+6:30
BangkokGMT+7
BarnaulGMT+7
ChristmasGMT+7
DavisGMT+7
HovdGMT+7
JakartaGMT+7
KrasnoyarskGMT+7
NovokuznetskGMT+7
NovosibirskGMT+7
Phnom PenhGMT+7
PontianakGMT+7
SaigonGMT+7
TomskGMT+7
VientianeGMT+7
BruneiGMT+8
CaseyGMT+8
Hong KongGMT+8
IrkutskGMT+8
Kuala LumpurGMT+8
KuchingGMT+8
MacauGMT+8
MakassarGMT+8
ManilaGMT+8
PerthGMT+8
ShanghaiGMT+8
SingaporeGMT+8
TaipeiGMT+8
UlaanbaatarGMT+8
EuclaGMT+8:45
ChitaGMT+9
DiliGMT+9
JayapuraGMT+9
KhandygaGMT+9
PalauGMT+9
PyongyangGMT+9
SeoulGMT+9
TokyoGMT+9
YakutskGMT+9
AdelaideGMT+9:30
Broken HillGMT+9:30
DarwinGMT+9:30
BrisbaneGMT+10
DumontDUrvilleGMT+10
GuamGMT+10
HobartGMT+10
LindemanGMT+10
MacquarieGMT+10
MelbourneGMT+10
Port MoresbyGMT+10
SaipanGMT+10
SydneyGMT+10
TrukGMT+10
Ust-NeraGMT+10
VladivostokGMT+10
Lord HoweGMT+10:30
BougainvilleGMT+11
EfateGMT+11
GuadalcanalGMT+11
KosraeGMT+11
MagadanGMT+11
NorfolkGMT+11
NoumeaGMT+11
PonapeGMT+11
SakhalinGMT+11
SrednekolymskGMT+11
AnadyrGMT+12
AucklandGMT+12
FijiGMT+12
FunafutiGMT+12
KamchatkaGMT+12
KwajaleinGMT+12
MajuroGMT+12
McMurdoGMT+12
NauruGMT+12
TarawaGMT+12
WakeGMT+12
WallisGMT+12
ChathamGMT+12:45
ApiaGMT+13
EnderburyGMT+13
FakaofoGMT+13
TongatapuGMT+13
KiritimatiGMT+14
Install
1npx stylexblocks add timezone-pickerSource
components/blocks/ark-ui/timezone-picker.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158import * as stylex from "@stylexjs/stylex";import { useMemo, useState } from "react";import { useListCollection } from "@ark-ui/react/collection";import { useFilter } from "@ark-ui/react/locale";import { Combobox, type ComboboxVariantProps } from "#components/ui/ark-ui/combobox.tsx";import { semanticColors } from "@stylexblocks/ui/semantic-tokens/colors.stylex.ts";import type { StyleOverrideProps } from "#lib/recipe-props.ts";
export interface Timezone { /** IANA zone id, e.g. "Europe/Amsterdam" — what your backend receives. */ value: string; /** City part of the zone id, e.g. "Amsterdam". */ label: string; /** Current UTC offset, e.g. "GMT+2" (DST-aware at render time). */ offset: string; /** Offset in minutes, for sorting. */ minutes: number;}
/** * Every IANA timezone the runtime knows, with current offsets — no shipped * data to go stale. Sorted by offset, then name. */export function getTimezones(): Timezone[] { const zones = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : ["UTC"]; const now = new Date(); return zones .map((zone) => { const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset", }).formatToParts(now); const offset = parts.find((p) => p.type === "timeZoneName")?.value ?? "GMT"; const m = offset.match(/GMT([+-])(\d{1,2})(?::(\d{2}))?/); const minutes = m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3] ?? 0)) : 0; const label = zone.split("/").pop()!.replaceAll("_", " "); return { value: zone, label, offset, minutes }; }) .sort((a, b) => a.minutes - b.minutes || a.label.localeCompare(b.label));}
const styles = stylex.create({ offset: { color: semanticColors.fgSubtle, fontSize: "0.8125em", marginInlineStart: "auto", paddingInlineStart: "0.75rem", },});
export interface TimezonePickerProps extends ComboboxVariantProps, StyleOverrideProps { label?: string; /** Trigger text while nothing is selected. */ placeholder?: string; searchPlaceholder?: string; /** IANA zone id of the initial selection. */ defaultValue?: string; /** IANA zone id of the controlled selection. */ value?: string; onValueChange?: (zone: string) => void; /** Posts the selected zone id with native form submits via a hidden input. */ name?: string; /** Override the offered zones, e.g. only your supported regions. */ items?: Timezone[]; disabled?: boolean;}
/** * TimezonePicker — select-style trigger that opens a panel with a search * input above the list (the "select with filter" pattern). The whole thing * is one combobox machine, not a Select with an input bolted in: the panel * input carries the ARIA combobox wiring (aria-expanded, aria-controls, * aria-activedescendant), so filtering stays screen-reader-accessible. * Zones and offsets come from Intl at render time. */export function TimezonePicker({ label = "Timezone", placeholder = "Select timezone", searchPlaceholder = "Search timezones…", defaultValue, value, onValueChange, name, items, disabled, variant = "outline", size = "md", palette, style,}: TimezonePickerProps) { const zones = useMemo(() => items ?? getTimezones(), [items]); const [inner, setInner] = useState(defaultValue ?? ""); const selected = value ?? inner; const selectedZone = zones.find((z) => z.value === selected);
const { contains } = useFilter({ sensitivity: "base" }); const { collection, filter, reset } = useListCollection({ initialItems: zones, itemToString: (z: Timezone) => `${z.label} (${z.offset})`, itemToValue: (z: Timezone) => z.value, // match the city name/offset, or anywhere in the IANA id ("europe/ams") filter: (itemText, filterText, item) => contains(itemText, filterText) || contains(item.value, filterText), });
return ( <Combobox.Root collection={collection} defaultValue={defaultValue ? [defaultValue] : undefined} value={value !== undefined ? (value ? [value] : []) : undefined} onValueChange={(details) => { const zone = details.value[0] ?? ""; setInner(zone); onValueChange?.(zone); }} onInputValueChange={(details) => filter(details.inputValue)} // the search is transient per open — a fresh panel shows every zone onOpenChange={(details) => { if (details.open) reset(); }} selectionBehavior="clear" disabled={disabled} variant={variant} size={size} palette={palette} style={style} > <Combobox.Label>{label}</Combobox.Label> <Combobox.Control> <Combobox.FieldTrigger> <Combobox.FieldValue placeholder={placeholder}> {selectedZone ? `${selectedZone.label} (${selectedZone.offset})` : null} </Combobox.FieldValue> </Combobox.FieldTrigger> </Combobox.Control> <Combobox.Positioner> <Combobox.Panel> <Combobox.Search placeholder={searchPlaceholder} /> <Combobox.PanelList> {collection.items.map((zone) => ( <Combobox.Item key={zone.value} item={zone}> <Combobox.ItemText>{zone.label}</Combobox.ItemText> <span {...stylex.props(styles.offset)}>{zone.offset}</span> <Combobox.ItemIndicator /> </Combobox.Item> ))} {collection.items.length === 0 ? ( <Combobox.Empty>No timezones found</Combobox.Empty> ) : null} </Combobox.PanelList> </Combobox.Panel> </Combobox.Positioner> {name ? <input type="hidden" name={name} value={selected} /> : null} </Combobox.Root> );}