# How to Add a Time Zone Converter to Your App — Copy-Paste Prompts Included

- Tool: App Features
- Last updated: July 2026

## TL;DR

A time zone converter needs the timezone package (Flutter) or Intl.DateTimeFormat (web) for DST-aware conversion, a Timer.periodic live clock ticking every second, a searchable IANA time zone picker, and a pinned zones list stored to Supabase or device shared_preferences. With FlutterFlow or Lovable you can ship a working world clock in 1–3 hours for $0/month. All time math runs on the client — no external APIs required.

## What a Time Zone Converter Actually Is

A time zone converter lets users see what time it is in multiple locations simultaneously — a live world clock with a pinnable list of cities. It is a utility feature for apps serving international users: scheduling tools, remote team dashboards, travel apps, and global marketplaces all benefit from it. The feature is entirely client-side: the timezone package (Flutter) or the browser's Intl.DateTimeFormat API (web) handles all DST-aware conversion without any external API calls. The product decisions are how many zones users can pin, how the IANA tz database is searched by city name, and whether pinned preferences are saved to Supabase or to device-local shared_preferences for an offline-first experience. The critical failure mode to avoid: manual UTC offset math that silently breaks during daylight saving time transitions.

## Anatomy of the Feature

Six components build a complete time zone converter. The timezone package initialization and DST-aware conversion are where first builds most often break silently.

- **Time Zone Search Picker** (ui): A searchable bottom sheet or dropdown backed by a static map of city display names to IANA timezone identifiers. In Flutter: a searchable ListView of city names using the timezone package's tz.getLocation() to validate identifiers; the map covers the 100 most searched cities (London, New York, Tokyo, Sydney, Paris, Dubai, etc.) with their IANA identifiers. In web apps: Intl.supportedValuesOf('timeZone') provides the raw IANA list; a display-name map makes results human-readable. The picker filters by city name as the user types and shows UTC offset alongside each result.
- **Live Clock Display** (ui): A text widget that updates every second via Timer.periodic(Duration(seconds: 1), (timer) => setState(() {})) in Flutter, or setInterval(update, 1000) in React. Converts DateTime.now() to the target timezone using TZDateTime.now(tz.getLocation(ianaId)) (Flutter) or new Intl.DateTimeFormat('en-US', {timeZone: ianaId, hour: '2-digit', minute: '2-digit', second: '2-digit'}) (web). Formats as HH:mm:ss for precision.
- **Pinned Time Zone List** (ui): A scrollable column of time zone cards. Each card shows the city display name, current time (HH:mm:ss), UTC offset badge, day-of-week, and date. Cards are ordered by the user's saved pin order. A delete/unpin swipe action removes the zone from the list. An 'Add' button at the bottom opens the Time Zone Search Picker.
- **Date Rollover Indicator** (ui): A conditional badge on each time zone card. Compares the calendar date of the target timezone against the user's local date: if the target's day-of-month is greater than local, show '+1 day'; if lesser, show '-1 day'. Calculated purely from TZDateTime.now() in the target zone vs DateTime.now().toLocal() — no hardcoded offset math.
- **Overlap Detector** (ui): An optional feature allowing the user to select two pinned zones and a time range to find the overlap window where both are in business hours (9 AM–5 PM local). Highlights the overlap window in green on a simple timeline. Pure client-side calculation: convert the business hours range of each zone to UTC, find the intersection of the two UTC intervals, display the result in each zone's local time. No API calls required.
- **User Preferences Store** (data): Two storage options depending on connectivity requirements. Option A (Supabase): user_timezone_prefs table (id, user_id, pinned_zones text[], home_zone text, updated_at) with RLS. Syncs pinned zones across devices. Option B (device-local): Flutter shared_preferences package storing pinned_zones as a JSON string under a stable key constant. Offline-first, no Supabase required. The brief recommends Supabase for cross-device sync and shared_preferences for offline-only apps.

## Data model

One table covers Supabase-backed user timezone preferences for cross-device sync. Run this in the Supabase SQL Editor if using Supabase for persistence — skip if using device-local shared_preferences.

```sql
create table public.user_timezone_prefs (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null unique,
  pinned_zones text[] not null default '{}',
  home_zone text,
  updated_at timestamptz not null default now()
);

alter table public.user_timezone_prefs enable row level security;

create policy "Users can view own timezone prefs"
  on public.user_timezone_prefs for select
  using (auth.uid() = user_id);

create policy "Users can insert own timezone prefs"
  on public.user_timezone_prefs for insert
  with check (auth.uid() = user_id);

create policy "Users can update own timezone prefs"
  on public.user_timezone_prefs for update
  using (auth.uid() = user_id);
```

The pinned_zones text[] column stores IANA timezone identifiers (e.g. ['America/New_York', 'Europe/London', 'Asia/Tokyo']). The UNIQUE constraint on user_id ensures one preferences row per user, enabling INSERT ... ON CONFLICT (user_id) DO UPDATE for upsert-style saves. home_zone stores the user's detected home timezone as an IANA identifier.

## Build paths

### Flutterflow — fit 5/10, 1–2 hours

Flutter's timezone and intl packages handle all IANA timezone math natively including DST. Timer.periodic drives the live clock. FlutterFlow Custom Widget for the live display. The fastest path for a native mobile world clock.

1. Add the timezone (^0.9.0) and intl packages to FlutterFlow under Settings → Custom Packages; also add shared_preferences if using device-local storage
2. Create a Custom Code init function that calls initializeTimeZones() — this must run before any widget tree is built; place it in the app's main initialization Custom Code block
3. Build a Custom Widget for the live clock: use Timer.periodic(Duration(seconds: 1), (_) => setState(() {})) and TZDateTime.now(tz.getLocation(ianaId)) for time conversion; cancel the timer in dispose()
4. Build the Time Zone Search Picker as a FlutterFlow bottom sheet with a TextField and a filtered ListView using a static Dart Map<String, String> of city display names to IANA identifiers
5. Store pinned zones in shared_preferences using a constant key name; on load, read the list and pass each IANA identifier to the live clock widget

Limitations:

- initializeTimeZones() must be called before any tz conversion — FlutterFlow Custom Code execution order can place it after the first frame; test on a real device to confirm no null reference errors on app launch
- The full IANA database via the timezone package adds about 1MB to the app bundle — acceptable for most apps but worth noting for bundle-size-sensitive use cases
- FlutterFlow Custom Widgets require Dart knowledge to write the Timer.periodic live clock; the built-in widget palette has no live-updating clock component

### Lovable — fit 3/10, 2–3 hours

Lovable builds the converter UI and pinned list quickly in React. Intl.DateTimeFormat covers all DST-aware conversion needs for web. Supabase stores user preferences for cross-device sync. Best if the converter is part of a web app rather than a standalone native mobile tool.

1. Create a Lovable project and paste the prompt below; the Intl.DateTimeFormat API is browser-native so no package installation is needed
2. If user preferences should persist across devices, connect Lovable Cloud and create the user_timezone_prefs table using the data model SQL in the Supabase SQL Editor
3. Test the live clock on the published URL — setInterval timing in the Lovable preview pane may behave differently than in a production browser tab
4. Verify DST-aware conversion by comparing the displayed time for 'America/New_York' against the correct time during a DST transition date
5. Use Design Mode to refine the card layout, UTC offset badge styling, and date rollover indicator colors without spending credits

Starter prompt:

```
Build a time zone converter feature in React. Use browser-native Intl.DateTimeFormat for all time zone conversion — no external libraries needed. Never use manual UTC offset math (Date.getTimezoneOffset() or adding hours manually) as this breaks during DST. Build: (1) a searchable Time Zone Picker dropdown backed by Intl.supportedValuesOf('timeZone') for the raw IANA list, plus a static Map of 50 common city display names to IANA identifiers for human-readable search; filter as user types, show city name and current UTC offset in dropdown, (2) a live clock using setInterval(() => setState(update), 1000) with Intl.DateTimeFormat(navigator.language, {timeZone: ianaId, hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false}).format(new Date()); clear interval in useEffect cleanup, (3) Pinned Zones List: scrollable column of cards showing city name, live time, UTC offset badge (Intl.DateTimeFormat with {timeZone: ianaId, timeZoneName: 'short'}.format()), day of week, date, and a date rollover indicator comparing the target date in the IANA zone against the local device date — computed with new Intl.DateTimeFormat('en-CA', {timeZone: ianaId}).format(new Date()) to get YYYY-MM-DD and compare with local, (4) minimum 5 pinnable zones; store pinned IANA identifiers in an array, (5) detect the user's local timezone on first load using Intl.DateTimeFormat().resolvedOptions().timeZone; if this returns undefined fallback to 'UTC' and show a warning banner, (6) save pinned_zones text[] and home_zone to Supabase user_timezone_prefs table on change using INSERT ... ON CONFLICT (user_id) DO UPDATE SET pinned_zones=$1, home_zone=$2, updated_at=now(); load on mount, (7) display UTC offset as 'GMT+X' or 'GMT-X' next to city name, (8) edge case when two pinned zones share the same IANA identifier: show them as one card, do not duplicate.
```

Limitations:

- Lovable targets web/PWA; native haptic feedback and device timezone detection are less reliable in a browser context than in a native Flutter app with the timezone package
- Intl.supportedValuesOf('timeZone') is not available in Safari versions before 15.4 — add a polyfill or filter to a curated list of 150 IANA identifiers as a fallback for older Safari users
- setInterval in a browser tab is throttled to 1-second-or-more intervals when the tab is backgrounded — the live clock pauses when the user switches tabs, which is expected browser behavior, not a bug

### Custom — fit 2/10, 1–3 days

Custom development is only warranted for meeting scheduler integration with Google Calendar API or a team directory showing each member's local time on their profile. A standalone time zone converter does not justify custom work.

1. Build the converter UI with FlutterFlow or Lovable (same as AI path) — the display layer is identical
2. Add Google Calendar API integration: OAuth flow for the user, fetch events for the next 7 days, display each event's time converted to every pinned zone
3. Add business hours indicator per pinned zone: a green/amber/red badge showing whether it is currently business hours (9 AM–5 PM local Monday–Friday) in that city
4. Add team directory integration: show each team member's current local time on their profile card using their stored IANA timezone from a team_members table

Limitations:

- Custom development for a standalone time zone converter is likely over-engineering — use FlutterFlow or Lovable unless deep calendar or team integration is a confirmed requirement
- Google Calendar API adds OAuth complexity and per-request costs that are disproportionate to the value of a simple world clock

## Gotchas

- **Displayed times are consistently wrong during daylight saving time transitions** — This is the defining bug of time zone converter builds. AI tools almost always implement time conversion with manual UTC offset arithmetic: DateTime.now().add(Duration(hours: -5)) for New York. This works in winter but silently shows the wrong time after DST changes — UTC-5 becomes UTC-4, but the hardcoded offset never updates. The bug is invisible until users in affected regions report that times are off by exactly 1 hour. Fix: Replace all manual offset math with the timezone package. Call TZDateTime.now(tz.getLocation('America/New_York')) — the package reads the IANA tz database which includes DST rules for every zone. Call initializeTimeZones() once before any conversion. Include 'DST-aware: the timezone package handles DST automatically via TZDateTime — do not use DateTime.add(Duration(hours: offset)) anywhere' in your prompt to explicitly block the manual approach.
- **initializeTimeZones() throws a null reference error on app launch** — FlutterFlow's Custom Code initialization runs in a specific order that may place initializeTimeZones() after the first widget frame is built. If any widget calls tz.getLocation() before initializeTimeZones() completes, the tz database returns null and the app crashes with 'Null check operator used on a null value' or 'TimeZoneNotFoundError'. Fix: Call initializeTimeZones() synchronously in the FlutterFlow app's main Custom Code block — the one that runs at app startup before the widget tree is built. Verify by adding a tz.getLocation('America/New_York') call immediately after initializeTimeZones() in the same block and confirming it returns a non-null Location object. If the error persists, wrap the widget tree in a FutureBuilder that awaits initializeTimeZones() before rendering.
- **Live clock stops updating after app is backgrounded and resumed** — Timer.periodic stops executing when the Flutter app is backgrounded on iOS — iOS suspends background Dart isolates after a few seconds. When the user returns to the app, the displayed time is frozen at the moment the app was backgrounded. The timer is still 'running' in the Dart sense but no ticks fired while backgrounded. Fix: Listen to AppLifecycleState via WidgetsBindingObserver. In didChangeAppLifecycleState: cancel the timer when the state is AppLifecycleState.paused and restart it when the state is AppLifecycleState.resumed. Restarting the timer on resume also forces an immediate clock update so the displayed time jumps to the correct current time instantly.
- **IANA timezone search returns no results for common city names** — The IANA database uses region/city format: Europe/London, Asia/Tokyo, America/New_York. If the search picker runs against the raw IANA identifier list, users typing 'London', 'Tokyo', or 'New York' get zero results. The app appears broken when it is simply missing a display-name translation layer. Fix: Maintain a static Map<String, String> of popular city display names to IANA identifiers and search against the display names. Store the IANA identifier internally for all conversions. A list of 50–100 cities covers the vast majority of real searches. Fall back to showing raw IANA identifiers (filtered by the search term) only if no display-name match is found.
- **Pinned zones list resets to empty after an app update** — shared_preferences keys are stored as strings. If the AI generates a key name that differs between app versions (e.g. 'pinnedZones' in v1 and 'pinned_zones' in v2), the v2 app reads null from shared_preferences on first launch and treats it as an empty list — all the user's saved zones appear to have been lost, even though the data is still in storage under the old key. Fix: Define the key as a constant: const kPinnedZonesKey = 'user_pinned_zones_v1'. On first launch after an update, check for the presence of any known old key names and migrate their values to the new constant key before reading. Increment the version suffix in the constant only intentionally as part of a deliberate migration — never casually rename it.

## Best practices

- Call initializeTimeZones() before any widget is built — it is a synchronous call that loads the IANA tz database into memory; skip it and every timezone conversion will crash or return wrong times
- Never use manual UTC offset arithmetic for DST-affected zones — always use TZDateTime.now(tz.getLocation(ianaId)) in Flutter or Intl.DateTimeFormat with the timeZone option on web
- Cancel Timer.periodic in the widget's dispose() method to prevent 'setState() called after dispose()' errors and memory leaks on navigation
- Maintain a static city-display-name-to-IANA-identifier map for the search picker — users search for 'Tokyo', not 'Asia/Tokyo'
- Use a stable constant for the shared_preferences key and add migration logic between app versions to copy values from old keys to new ones before reading
- Show the UTC offset next to each city name (GMT+X or GMT-X) so users can quickly verify the math rather than trusting the displayed time blindly
- Listen to AppLifecycleState and restart the live clock timer on AppLifecycleState.resumed to ensure the displayed time is correct immediately after the user returns to the app

## Frequently asked questions

### Does this work without internet connection?

Yes — all timezone conversion math runs locally using the timezone package's bundled IANA database. The timezone package includes DST rules for all zones compiled into the app bundle at build time. No network call is needed for time conversion. If you use shared_preferences for pinned zones storage, the pinned list is also available offline. Supabase sync for cross-device preferences requires connectivity but gracefully falls back to the locally cached list when offline.

### How does daylight saving time affect the displayed times?

The timezone package and Intl.DateTimeFormat both use the IANA tz database which contains the complete DST transition history and future rules for every timezone. When DST changes, the displayed time automatically reflects the correct offset without any code change. The bug to avoid is manual offset arithmetic (e.g. 'New York is always UTC-5') — that breaks twice a year. Always use the package, never hardcode offsets.

### Can I add any city in the world?

You can add any IANA timezone identifier — there are about 600 in the full database. For the search picker to work by city name, the city must be in your display-name map. The prompt requests a 50-city map covering the most common searches. For less common cities, users can search by UTC offset (e.g. 'UTC+9' surfaces Asia/Tokyo and Asia/Seoul) or browse the full list. You can expand the map with more cities after the initial build.

### How many time zones can I pin?

The schema allows any number — pinned_zones is a text[] array with no database constraint. The prompt sets a soft limit of 10 pinned zones with an error message when the user tries to exceed it. The UI becomes unwieldy past about 7–8 cards on a mobile screen. You can adjust this limit in the prompt — just specify the max you want enforced.

### What happens when two pinned cities are in the same time zone?

Both appear as separate cards showing identical times. The prompt includes an edge case handler for this: if two IANA identifiers in the pinned list resolve to the same current UTC offset and DST state, they are collapsed into one card showing both city names. Specify this behavior in the prompt if you want it — otherwise both cards render independently.

### How do I detect my current local time zone automatically?

In Flutter: DateTime.now().timeZoneName gives the device's IANA timezone name on most platforms. In web: Intl.DateTimeFormat().resolvedOptions().timeZone returns the IANA identifier from the browser's timezone setting. If the device timezone is unavailable (returns null or empty string), the app falls back to UTC and shows a 'Using UTC as default' banner. Auto-detection runs on first app launch and sets the home_zone field.

### Can this feature help me schedule meetings across time zones?

The optional Overlap Detector component finds windows where two pinned zones are both in business hours (9 AM–5 PM local). For multi-person scheduling with actual calendar availability — checking who is free at a given time based on real calendar events — you would need Google Calendar API or Calendly integration. That is a custom development requirement separate from the converter itself.

### Is UTC the same as GMT?

For display purposes, yes — UTC and GMT are the same offset (zero hours from Coordinated Universal Time). The technical difference is that GMT is a timezone (it observes BST in summer), while UTC is a standard that never changes. In the IANA database, 'Etc/UTC' and 'GMT' are both valid identifiers. Convention in app UIs is to display UTC offsets as 'GMT+5' or 'GMT-3' rather than 'UTC+5' because users recognize GMT more readily.

---

Source: https://www.rapidevelopers.com/app-features/time-zone-converter
© RapidDev — https://www.rapidevelopers.com/app-features/time-zone-converter
