Build an isochrone map with the Google Isochrone API
Use the Google Maps Isochrone API to build an isochrone map that shows how far you can drive, bike, or walk in a set amount of time.
In this post, you'll use the Google Isochrone API to build a map of everywhere you can drive, bike, or walk to in a set amount of time. Use it to find an apartment near work, a hotel near the conference center, or the best site for a new store. Like most of our tutorials, I've included full source code (frontend / backend - coming soon!) that you can use in your own projects.

Part 1: Mapping travel time with the Google Maps Isochrones API
Part 2: How isochrones work: From shortest paths to polygons
Part 3: Build an isochrone map with the Google Isochrone API (this article)

What is an isochrone map?
An isochrone map calculates the area reachable within a specified amount of time from a given location. And because it follows the real road network instead of drawing a straight line radius, it captures the roads, highways, traffic patterns, and transportation modes that affect how long it takes to get somewhere.
Building an isochrone map using the Google Isochrone API

In this post, you’ll build a copy of the official Google Maps Isochrones API demo. Users enter a starting location, choose a travel mode (drive, bike, or walk), and set a travel time to see the area they can reach. The app also supports the API’s advanced options: live traffic, edge smoothing, and polygon fidelity.
How the code for our isochrone map is organized
There are two parts to the app, a web frontend (google_isochrones_frontend - coming soon!) that handles user input and renders the isochrone, and a small backend (google_isochrones_backend - coming soon!) that makes the call to the Isochrones API.

App.jsx
App.jsx is the main entryway (via main.jsx) to our app. It wraps everything in <APIProvider/>, which loads the Google Maps API, and renders <HomePage/>.
/*** App.jsx ***/
import React from "react";
import { APIProvider } from "@vis.gl/react-google-maps";
import HomePage from "./pages/HomePage";
const API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY || "";
function App() {
return (
<APIProvider apiKey={API_KEY}>
<HomePage />
</APIProvider>
);
}
export default App;HomePage.jsx
The <HomePage/> component does a whole lot more than just render <MapView/> (the Google Maps layer), <LocationAutocomplete/> (the address selector) and the sidebar configuration panel. It holds the app state: origin (markerPosition), travelMode, travelDuration, smoothing and real time traffic settings (routingPreference), and much more. It also triggers a new isochrone request whenever any of those change.
/*** pages/HomePage.jsx ***/
// Fetch a new isochrone whenever the origin or any setting changes
useEffect(() => {
if (!markerPosition) return;
let cancelled = false;
fetchIsochrone({
position: markerPosition,
travelMode,
travelDuration,
routingPreference,
enableSmoothing,
}).then((data) => {
if (!cancelled && data) setPolygonData(data);
});
// If settings change before this request finishes, ignore its response
return () => {
cancelled = true;
};
}, [
markerPosition,
travelMode,
travelDuration,
routingPreference,
enableSmoothing,
fetchIsochrone,
]);
After each render, React compares the values in the useEffect dependency array with their previous values. If any have changed, it re-runs the effect. So once an origin is set, clicking a different travelMode, travelDuration, toggle or enableSmoothing updates a piece of state and triggers a new request.

The sidebar is a fixed width column on the right of the screen, split into two stacked areas. The top area holds the controls; the bottom area is the API panel, which fills whatever height is left. The controls section turns each click into a state update, which triggers the fetch effect. The API panel displays the curl command and the response, with copy buttons.
/*** pages/HomePage.jsx ***/
<div className="HomePage__sidebar">
<div className="HomePage__controls">
{/* Origin: search box sets the isochrone's starting point */}
<LocationAutocomplete
onSelect={handleLocationSelect}
currentName={locationName}
hasMarker={!!markerPosition}
/>
{/* Travel mode: one button per mode; clicking sets state */}
{TRAVEL_MODES.map((m) => (
<button key={m.value} onClick={() => setTravelMode(m.value)} className={...active if selected}>
<img src={m.icon} alt="" />
</button>
))}
{/* Travel time: same pattern */}
{DURATIONS.map((d) => (
<button key={d.seconds} onClick={() => setTravelDuration(d.seconds)} className={...}>
{d.label}
</button>
))}
{/* Live traffic toggle: disabled unless travel mode is DRIVE */}
<div
role="switch"
aria-checked={!liveDisabled && routingPreference === 'TRAFFIC_AWARE'}
onClick={() => {
if (liveDisabled) return;
setRoutingPreference(routingPreference === 'TRAFFIC_AWARE' ? 'TRAFFIC_UNAWARE' : 'TRAFFIC_AWARE');
}}
/>
{/* Smooth edges toggle */}
<div role="switch" aria-checked={enableSmoothing} onClick={() => setEnableSmoothing(!enableSmoothing)} />
{loading && <div><SpinnerIcon /> Generating...</div>}
</div>
{/* API panel: two tabs */}
<div className="HomePage__api-panel">
<button onClick={() => setActiveTab('request')}>API Request</button>
<button onClick={() => setActiveTab('response')}>API Response</button>
{activeTab === 'request' ? (
<>
<button onClick={handleCopyRequest}>Copy</button>
<pre>{curlCommand}</pre>
</>
) : (
<>
{rawResponse && <button onClick={handleCopyResponse}>Copy</button>}
<pre>{rawResponse ? JSON.stringify(rawResponse, null, 2) : 'Generate an isochrone to see the API response...'}</pre>
</>
)}
</div>
</div>Keeping the sidebar inline is simpler because all the settings state lives in HomePage.jsx, and the sidebar reads and modifies almost all of it. Kept inline, the buttons and toggles can read values like travelMode and call setters like setTravelMode and setTravelDuration directly, without them being passed down as props. For a simple single page app, that’s more than enough.
LocationAutocomplete.jsx

The <LocationAutocomplete/> component, as the name suggests, contains a text input field that uses Google Places Autocomplete to suggest places dynamically as the user types. Here's how this works:
- The component renders a plain text
<input>and attaches a ref to it withref={inputRef}, whereinputRefis created withuseRef(null). Once the component is on the page,inputRef.currentpoints to the actual input element, which is what Google’s Autocomplete needs to attach to.
/*** components/LocationAutocomplete.jsx ***/
return (
<div className="LocationAutocomplete">
<div className="LocationAutocomplete__input-wrap">
<SearchOutlineIcon className="LocationAutocomplete__search-icon" />
<input
ref={inputRef}
type="text"
defaultValue={hasMarker ? currentName : ""}
onFocus={handleFocus}
placeholder="Search for a location..."
className="LocationAutocomplete__input"
/>
</div>
</div>
);
- A
useEffecthook waits for the Google Places library to finish loading (useMapsLibrary('places')returnsnulluntil it’s ready), then creates an Autocomplete instance on the input:
autocompleteRef.current = new placesLib.Autocomplete(inputRef.current);- The effect also adds a listener for the
place_changedevent to the Autocomplete instance. This event fires when the user selects a suggestion from the dropdown. The handler callsgetPlace()to get the selected place, ignores it if it has no coordinates, and passes its position ({ lat, lng }) andnameto theonSelectprop. InHomePage.jsx,onSelectishandleLocationSelect, which updatesmarkerPositionandlocationName, and that state change triggers a new isochrone request.
/*** components/LocationAutocomplete.jsx ***/
useEffect(() => {
if (!placesLib || !inputRef.current || autocompleteRef.current) return;
autocompleteRef.current = new placesLib.Autocomplete(inputRef.current);
const listener = autocompleteRef.current.addListener("place_changed", () => {
const place = autocompleteRef.current?.getPlace();
if (!place?.geometry?.location) return;
const name = place.name || place.formatted_address || "";
const position = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
};
suppressFillRef.current = true;
onSelectRef.current(position, name);
});
return () => {
google.maps.event.removeListener(listener);
autocompleteRef.current = null;
};
}, [placesLib]);
useIsochrone.js
Whenever the marker position changes (because the user selected a place in the search box or clicked the map), or any of the isochrone settings change, a useEffect in HomePage.jsx calls fetchIsochrone(). This function is returned by the useIsochrone() hook in hooks/useIsochrone.js. It builds the request and calls generateIsochrone() in api/isochrones.js, which sends it to the backend with an axios POST to the /isochrones endpoint.
/*** hooks/useIsochrone.js ***/
import { generateIsochrone } from "../api/isochrones";
const fetchIsochrone = useCallback(async (params) => {
setLoading(true);
setError(null);
try {
const request = {
latitude: params.position.lat,
longitude: params.position.lng,
travelMode: params.travelMode,
travelDuration: params.travelDuration,
routingPreference: params.routingPreference,
enableSmoothing: params.enableSmoothing,
...(params.polygonFidelity ? { polygonFidelity: params.polygonFidelity } : {}),
};
const data = await generateIsochrone(request);
// A 204 "no content" response doesn't throw in axios, it arrives as an empty body
if (!data?.polygons?.length) {
setError('No reachable area found for these parameters.');
return null;
}
return data;
} catch (err) {
if (axios.isAxiosError(err) && err.response) {
setError(err.response.data?.message || `Server error (HTTP ${err.response.status})`);
} else {
setError(err.message || 'An unexpected error occurred.');
}
return null;
} finally {
setLoading(false);
}
}, []);
const clearError = useCallback(() => setError(null), []);
return { loading, error, fetchIsochrone, clearError };The useIsochrone() hook returns an object with four properties, the most important is fetchIsochrone, a function that sends the request to the backend, which then calls Google’s Isochrones API. It returns a promise that resolves to the response containing the isochrone polygon (data.polygons, below), or to null if the request fails.
{
"polygons": [
{
"travelDuration": "1800s",
"travelMode": "BICYCLE",
"geojson": {
"type": "MultiPolygon",
"coordinates": [
[
[
[
-123.10366188933581,
49.256439060502814
],
[
-123.1018689929727,
49.25634289023366
],
[
-123.09907399005446,
49.25656971737912
],
//... 157 more entries
]
]
]
}
}
}
}The useEffect in HomePage.jsx saves it into polygonData state with setPolygonData, and passes polygonData as a prop to MapView.jsx.
/*** pages/HomePage.jsx ***/
<MapView
markerPosition={markerPosition}
onMapClick={handleMapClick}
polygonData={polygonData}
/>;
MapView.jsx

MapView.jsx renders a <Map /> component centered on Vancouver, BC. Once the user picks an origin (by search or by clicking the map), it shows a marker there and draws the isochrone polygon around it. It does this by passing in polygonData as a prop to the child <Isochrone/> component.
/*** components/MapView.jsx ***/
return (
<Map
className="MapView"
defaultCenter={{ lat: 49.25363726536889, lng: -123.1114298669681 }}
defaultZoom={12}
mapTypeControl={false}
streetViewControl={false}
fullscreenControl
fullscreenControlOptions={{ position: 5 }}
zoomControl
zoomControlOptions={{ position: 5 }}
onClick={handleClick}
styles={[
{ featureType: "poi", stylers: [{ visibility: "off" }] },
{ featureType: "transit", stylers: [{ visibility: "off" }] },
]}
>
{markerPosition && <Marker position={markerPosition} />}
<Isochrone data={polygonData} />
</Map>
);
Isochrone.jsx
<Isochrone /> draws the isochrone as a shaded area on the map. It doesn’t render any HTML of its own (it returns null). Instead, it draws directly on the map using the Google Maps API.
First, it gets the map with useMap(). Then, inside a useEffect, it:
- Takes the
geojsonfrom the first entry indata.polygons, - Creates a
google.maps.Datalayer on the map, - Wraps the GeoJSON in a
Featureand adds it withaddGeoJson, and - Styles it light blue with a blue outline with
setStyle.
/*** components/Isochrone.jsx ***/
import { useEffect } from "react";
import { useMap } from "@vis.gl/react-google-maps";
//... Function to compute isochrone bounding box
function Isochrone({ data }) {
const map = useMap();
useEffect(() => {
if (!map || !data?.polygons?.length) return;
const { geojson } = data.polygons[0];
const layer = new google.maps.Data({ map });
layer.addGeoJson({
type: "Feature",
geometry: geojson,
properties: {},
});
layer.setStyle({
fillColor: "#3B82F6",
fillOpacity: 0.15,
strokeColor: "#2563EB",
strokeWeight: 2,
});
//... Bounds calculation
return () => layer.setMap(null);
}, [map, data]);
return null;
}
export default Isochrone;
HomePagepassespolygonDataas a prop toMapView.MapViewpasses it to<Isochrone />asdata={polygonData}.- In a
useEffect,Isochronegets the map instance withuseMap()and takes thegeojsonfield from the first entry indata.polygons. - It creates a
google.maps.Datalayer on the map, wraps the GeoJSON geometry in aFeature, and adds it withaddGeoJson.
Deploy and run
To test the app, simply visit https://google-isochrones-demo.afi.dev/ and enter an address into the search box on the top right. If you'd like to run the app locally, fork both google_isochrones_frontend (coming soon!) and google_isochrones_backend (coming soon!) on GitHub and run npm install to install dependencies. Start the app by running npm run dev in each folder, then open http://localhost:5173/ in your browser to view it. Remember to update the .env files in both repos with a Google Maps API key that has both the Places API and Isochrones API enabled.
Isochrone maps in real world apllications
At the time of writing (November 2026), the Isochrones API is still in pre-GA Preview, and it has two notable gaps. It doesn’t support public transit, so it can’t account for bus or train schedules. And there’s no way to set a departure time. Live traffic reflects conditions at the moment you make the request, so you can’t ask, for example, how far you could drive at 8am on a weekday during rush hour.
That being said, its still incredibly useful. A property search portal could use it to help users look for homes within a 30-minute commute of their office or their children’s school, which is what most buyers and renters actually care about. City planners can use isochrones to see how much of a city can reach a hospital, a school or a park on foot or by bike, and where those services are hard to get to. If you have new or interesting use cases for the Isochrones API, or have general questions about the Google Maps APIs, please reach out!
Part 1: Mapping travel time with the Google Maps Isochrones API
Part 2: How isochrones work: From shortest paths to polygons
Part 3: Build an isochrone map with the Google Isochrone API
This article was written by Afi Labs, a Google Maps Premier Partner and reseller. We build route optimization, navigation, and fleet tracking software on Google Maps, and offer volume pricing on GMP licensing. Talk to an engineer or follow Afian on LinkedIn.
