> ## Content Index
> Fetch the complete content index at: https://blog.afi.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Build an isochrone map with the Google Isochrone API
- URL: https://blog.afi.io/blog/build-an-isochrone-map-with-the-google-isochrone-api/
- Published: 2026-08-09T02:15:00.000Z
- Updated: 2026-11-11T02:17:48.000Z
- Description: 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.
- Author: Afian Anwar
- Tags: googlemaps

In this post, you'll use the [Google Isochrone API](https://developers.google.com/maps/documentation/isochrones/overview?ref=blog.afi.io) 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.

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2026/11/GoogleMap-Isochrone--3-.png)

Part 1: [Mapping travel time with the Google Maps Isochrones API](https://blog.afi.io/blog/mapping-travel-time-with-the-google-maps-isochrones-api/)  
Part 2: [How isochrones work: From shortest paths to polygons](https://afi.io/blog/how-isochrones-work-from-shortest-paths-to-polygons?ref=blog.afi.io)  
**Part 3: Build an isochrone map with the Google Isochrone API (this article)**

[Show how far you can travel from a point in a set amount of time using the Google Isochrones APIMapping travel time and reachability with the Google Isochrones API![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/icon/favicon-91f11958-286f-4d50-b18a-5665c461826f.png)![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/thumbnail/logo512-a0431761-f71c-48a0-a9ed-4771baea933b.png)](https://google-isochrones-demo.afi.dev/?ref=blog.afi.io)

### 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

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2026/11/isochrones-mockup.png)

In this post, you’ll build a copy of the official [Google Maps Isochrones API demo](https://developers.google.com/maps/documentation/isochrones/demo?ref=blog.afi.io). 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](https://developers.google.com/maps/documentation/isochrones/overview?ref=blog.afi.io).

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2026/11/image.png)

**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.

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2026/11/image-1.png)

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**

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2026/11/Screenshot-2026-11-09-at-4.16.14---PM.png)

The `<LocationAutocomplete/>` component, as the name suggests, contains a text input field that uses [Google Places Autocomplete](https://blog.afi.io/blog/google-address-autocomplete-with-the-places-api/) to suggest places dynamically as the user types. Here's how this works:

1. The component renders a plain text `<input>` and attaches a ref to it with `ref={inputRef}`, where `inputRef` is created with `useRef(null)`. Once the component is on the page, `inputRef.current` points 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>
);

```

1. A `useEffect` hook waits for the Google Places library to finish loading (`useMapsLibrary('places')` returns `null` until it’s ready), then creates an Autocomplete instance on the input:

```
autocompleteRef.current = new placesLib.Autocomplete(inputRef.current);
```

1. The effect also adds a listener for the `place_changed` event to the Autocomplete instance. This event fires when the user selects a suggestion from the dropdown. The handler calls `getPlace()` to get the selected place, ignores it if it has no coordinates, and passes its position (`{ lat, lng }`) and `name` to the `onSelect` prop. In `HomePage.jsx`, `onSelect` is `handleLocationSelect`, which updates `markerPosition` and `locationName`, 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**

![](https://storage.ghost.io/c/c6/4d/c64da7e8-63a6-4cff-acdc-2782a6ebc377/content/images/2026/11/Screenshot-2026-11-11-at-8.09.19---AM.png)

`MapView.jsx` renders a [<Map /> component](https://visgl.github.io/react-google-maps/docs/api-reference/components/map?ref=blog.afi.io) 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:

1. Takes the `geojson` from the first entry in `data.polygons`,
2. Creates a `google.maps.Data` layer on the map,
3. Wraps the GeoJSON in a `Feature` and adds it with `addGeoJson`, and
4. 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;

```

1. `HomePage` passes `polygonData` as a prop to `MapView`.
2. `MapView` passes it to `<Isochrone />` as `data={polygonData}`.
3. In a `useEffect`, `Isochrone` gets the map instance with `useMap()` and takes the `geojson` field from the first entry in `data.polygons`.
4. It creates a `google.maps.Data` layer on the map, wraps the GeoJSON geometry in a `Feature`, and adds it with `addGeoJson`.

### Deploy and run

To test the app, simply visit [https://google-isochrones-demo.afi.dev/](https://google-isochrones-demo.afi.dev/?ref=blog.afi.io) 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/](http://localhost:5173/?ref=blog.afi.io) 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](https://developers.google.com/maps/documentation/isochrones/overview?ref=blog.afi.io), or have general questions about the Google Maps APIs, please [reach out](https://www.afi.io/contact%5Fus?ref=blog.afi.io)!

Part 1: [Mapping travel time with the Google Maps Isochrones API](https://blog.afi.io/blog/mapping-travel-time-with-the-google-maps-isochrones-api/)  
Part 2: [How isochrones work: From shortest paths to polygons](https://afi.io/blog/how-isochrones-work-from-shortest-paths-to-polygons?ref=blog.afi.io)  
Part 3: [Build an isochrone map with the Google Isochrone API](https://blog.afi.io/blog/build-an-isochrone-map-with-the-google-isochrone-api/)

**This article was written by** [**Afi Labs**](https://afi.io/?ref=blog.afi.io)**, a** [**Google Maps Premier Partner**](https://www.afi.io/?ref=blog.afi.io) **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**](https://www.afi.io/contact%5Fus?ref=blog.afi.io) **or** [**follow Afian on LinkedIn**](https://www.linkedin.com/in/afian-anwar/?ref=blog.afi.io)**.**