Timezone Select React component for Tailwind CSS v4
v1.0.0Added in 1.10.0
A searchable time zone picker with live UTC offsets, optional local times, region groups and a one-tap Use my time zone row.
Click or focus the field and the list opens with every time zone, grouped by region (or by offset), each row showing its UTC offset and, with showTime, the current time there, ticking each minute while the list is open. The first row, "Use my time zone", picks the browser's zone in one go, and suggested zones sit right under it. The field text is selected on focus, so typing replaces it with a search: a city ("tokyo"), a country for common zones ("india"), an abbreviation ("EST", "CET") or an offset ("+2", "utc-5", "+5:30") all find their zone, with the matching letters in bold. Offsets and names follow daylight saving time for the date you pass, or for today. Leaving a search without a pick (Escape, a click away) brings back the zone you had. Keyboard: ArrowDown and ArrowUp move, PageUp / PageDown jump, Enter picks, Escape closes. On a phone a tap opens a full-height search sheet with 48px rows and a Cancel button; a tap on a row fills the field and closes it.
Timezone Select is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add timezone-select, together with Combobox, Text match, useControllableState and UIProvider and the npm package lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add timezone-selectPro item: the CLI needs a Pro account. See plans
Usage
Import TimeZoneSelect from @/components/ui/timezone-select.
import { useState } from "react"
import { Globe } from "lucide-react"
import { TimeZoneSelect } from "@/components/ui/timezone-select"
export function ProfileTimeZone() {
const [timeZone, setTimeZone] = useState("America/New_York")
return (
<TimeZoneSelect
label="Time zone"
description="Used for reminders and weekly reports."
leftIcon={<Globe />}
value={timeZone}
onValueChange={setTimeZone}
suggested={["America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles"]}
suggestedLabel="United States"
showTime
name="timeZone"
/>
)
}Props
labeldefault""ReactNodeThe label above the field; it also names the list and the phone sheet.
descriptiondefault""ReactNodeHelper text under the field, linked with aria-describedby.
errordefault""ReactNodeReplaces the description with an error and marks the field invalid.
requireddefaultfalsebooleanAdds the asterisk to the label and required to the input.
invaliddefaultfalsebooleanThe invalid look without a message.
disableddefaultfalsebooleanDims the field and blocks it.
readOnlydefaultfalsebooleanShows the zone without letting anyone change it.
valuestringThe chosen IANA id, like "America/New_York", when you control it. Older ids ("Asia/Calcutta", "Europe/Kiev") are read as their current names.
defaultValuedefault""stringThe starting zone, uncontrolled. "" is empty.
onValueChange(value: string) => voidCalled with the IANA id of the picked zone (the browser's zone for the Use my time zone row).
timeZonesstring[]The zones to offer. Default: every zone the browser knows (Intl.supportedValuesOf), with UTC added, or a built-in list of about 55 common zones on older runtimes.
groupBydefault"region""region" | "offset" | "none"region groups by the id's continent (Americas, Europe, Asia...) sorted by city; offset groups by UTC offset from west to east; none is one flat list from west to east.
showOffsetdefaulttruebooleanThe UTC offset at the end of each row, like "UTC−04:00". Grouping by offset puts it in the group titles instead.
showTimedefaultfalsebooleanThe current local time in each row, refreshed every minute while the list is open.
formatdefault"long""long" | "short"long puts the zone name under the city in each row ("Eastern Daylight Time"); short shows the city only. The name stays searchable either way, and the field always shows the city.
suggestedstring[]Zones listed first under suggestedLabel, like the ones your team uses. They move out of their region group.
suggestedLabeldefault"Suggested"stringThe title of the suggested group.
detectdefaulttruebooleanAdds a first row that picks the browser's own time zone, when it is in the list.
detectLabeldefault"Use my time zone"stringThe text of the detect row; the detected zone shows under it.
dateDateThe instant used for offsets, names and times, so daylight saving time is right for a meeting next month. Default: now.
localedefault""stringLocale for zone names and times through Intl. Falls back to the UIProvider locale, then to the browser's. City names stay as in the IANA id.
regionLabelsPartial<Record<string, string>>Group titles keyed by the id's first part: America, Europe, Africa, Asia, Australia, Pacific, Atlantic, Indian, Antarctica, Arctic and Other (UTC). Defaults in TIME_ZONE_REGION_LABELS.
placeholderdefault"Select a time zone"stringShown while the field is empty, also in the phone sheet's search box.
emptyTextdefault"No time zones found"stringShown when the search matches nothing.
clearabledefaultfalsebooleanAn x while a zone is chosen; Escape on a closed list clears too.
clearLabeldefault"Clear"stringThe accessible name of the x button.
toggleLabeldefault"Show time zones"stringThe accessible name of the chevron button.
openOnFocusdefaulttruebooleanOpens the full list as soon as the field gets focus, and again on a click, like a select. Off: the list opens when you type, press ArrowDown or use the chevron.
autoHighlightdefaulttruebooleanThe first match is highlighted while you search, so Enter picks it.
highlightMatchesdefaulttruebooleanThe matched letters of each zone in bold.
fuzzydefaultfalsebooleanAlso matches letters in order with gaps ("lsa" finds Los Angeles), ranked last.
leftIconReactNodeAn icon before the text, like a Globe. Unsized svgs follow the size.
sizedefault"md""sm" | "md" | "lg"32 / 40 / 48px like every field; md grows to 44px on touch screens.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The box: a filled well, an outline, a bottom line, or nothing until hover.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape, scaled per size like the button (md is 8 / 10 / 12px).
defaultOpendefaultfalsebooleanStarts with the list (or the sheet) open. Pass open and onOpenChange to control it.
openbooleanWhether the list is open, controlled.
onOpenChange(open: boolean) => voidCalled when the list opens or closes.
responsivedefaulttruebooleanBelow 768px the field opens a full-height search sheet instead of the popover.
sheetTitledefault"Time zone"stringThe phone sheet's title for screen readers (its visible header is the search box); a string label wins.
cancelLabeldefault"Cancel"stringThe text button next to the sheet's search box.
sidedefault"bottom""top" | "right" | "bottom" | "left"Where the list opens; it flips when there is no room.
aligndefault"start""start" | "center" | "end"How the list lines up with the field.
maxHeightdefault360numberThe list's height limit in px, capped by the room on screen.
virtualThresholddefault100numberAbove this many rows only the visible ones render (the full list has about 420).
renderOption(option, state: { selected, highlighted, disabled, ranges }) => ReactNodeReplaces a row's content; the row element, its aria and its classes stay.
namedefault""stringSubmits the IANA id through a hidden input.
idstringThe input's id; generated when left out.
onChange(event: ChangeEvent<HTMLInputElement>) => voidThe native event of every keystroke in the search, untouched.
containerHTMLElement | nullPortal target for the list and the sheet (default document.body).
fieldPropsPartial<FieldProps>Anything else for the field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.
classNamesPartial<Record<"field" | "label" | "description" | "error" | "counter" | "root" | "box" | "input" | "leftIcon" | "spinner" | "clear" | "toggle" | "content" | "list" | "item" | "group" | "status" | "sheet" | "sheetSearch" | "cancel", string>>Extra classes for the inner parts, the same slots as the Combobox. content is the popover, sheet the phone sheet.
classNamestringExtra classes for the outermost element: the Field when there is a label or message, else the combobox root.
refRef<HTMLInputElement> (a useRef object or an (el) => void callback)The search input. Other input props (onBlur, onKeyDown, autoFocus, aria-*, data-*) land on it too.
Parts
getTimeZoneOptions
functionBuilds the option list the component uses: every zone with its city, region, offset, localized name, local time and search keywords, sorted west to east. It reads Intl, so call it in an effect or an event handler, not in a server render. Reuse it for your own list or a settings page.
timeZonesreadonly string[]The ids to describe. Default: every zone the runtime knows plus UTC.
datedefault"new Date()"DateThe instant for offsets, names and times.
localestringLocale for names and times.
formatdefault"long""long" | "short"How label is built.
returnsTimeZoneOption[]One option per zone.
formatUtcOffset
functionThe offset of a zone at an instant as text: formatUtcOffset("America/New_York", date) gives "UTC−04:00" in summer and "UTC−05:00" in winter, and "" for an unknown id.
timeZone*stringAn IANA id.
datedefault"new Date()"DateThe instant.
prefixdefault"UTC"stringText before the sign, e.g. "GMT".
getUtcOffset
functionThe offset of a zone at an instant in minutes east of UTC (-240 for New York in summer, 330 for Kolkata), NaN for an unknown id.
timeZone*stringAn IANA id.
datedefault"new Date()"DateThe instant.
TIME_ZONE_REGION_LABELS
constantThe default group titles keyed by the id's first part (America: Americas, Indian: Indian Ocean, Other for UTC).
TimeZoneOption
type{ value, city, region, offset, offsetLabel, name, label, time, keywords }: one zone as getTimeZoneOptions returns it.
GetTimeZoneOptionsParams
typeThe parameters of getTimeZoneOptions.
TimeZoneGroupBy
type"region" | "offset" | "none".
TimeZoneFormat
type"long" | "short".
TimeZoneSelectProps
typeThe props of TimeZoneSelect: its own plus the Combobox field props it passes through.
Examples
With local time
showTime adds the current time to each row and ticks each minute while the list is open.
Grouped by offset
groupBy="offset" lists zones west to east under their UTC offset.
Suggested zones for a US team
suggested pins the zones your team uses to the top under a title you choose.
Offsets for a future date
date picks the instant for offsets and times, so a January meeting shows winter time.
In a settings form
required, a validation error, clearable and a hidden input for native forms.
Files
- components/ui/timezone-select.tsx
FAQ
Is the React Timezone Select component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Timezone Select?
Run npx wingo-ui@latest add timezone-select. It also adds the Wingo UI items it builds on (Combobox, Text match, useControllableState and UIProvider) and the npm package lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/timezone-select.
How does Timezone Select behave on phones?
Below 768px the field opens a full-height search sheet instead of the popover. The responsive prop controls this and is on by default.