# React Location Picker: Draggable Pin, Radius, Search

> Build a React location picker on a real map: a draggable pin, a radius with a slider, address search, a phone sheet and a static card if the map fails.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-location-picker

## TL;DR

A React location picker needs a pin you can click, drag and move with the keyboard, a search that flies the map to a place, and an optional radius circle for delivery zones or nearby filters. It also has to load the map library only in the browser, keep the page scroll working on phones, and show something useful when the tiles never arrive. The Wingo UI Location Map does all of this on Leaflet and OpenStreetMap with a controlled value and radius, plus a StaticMap card for emails and long lists.

You need a React location picker: a listing form that asks where the apartment is, a checkout that asks where to leave the parcel, a search page that filters by distance. A text field for the address works until the building has no number, the courier needs the back entrance, or the filter is "within 10 miles". Then you need a real map with a pin. This guide covers what a picker has to handle, the props that matter in the Wingo UI [Location Map](https://wingo-ui.com/components/location-map), and full examples for a pin, a radius and address search.

## What should a React location picker handle?

Seven things. If you search for "pick location on map react", the typical answer is a react-leaflet `useMapEvents` click handler that drops a marker, which covers only the first one. Here is the list we hold our own react map picker to:

1. **Three ways to place the pin.** Click or tap the map, drag the pin, and move it with the arrow keys once it has focus.
2. **Search.** Type a town or an address, pick a result, and the map flies there.
3. **A radius** for delivery zones and nearby filters, with a slider that resizes the circle live.
4. **My location**, with an honest message when the browser refuses.
5. **A scroll that still works.** A map that grabs the wheel or a one-finger swipe traps the reader halfway down the page.
6. **A server-safe load.** Leaflet touches `window` on import, which crashes a Next.js server render.
7. **A fallback** when tiles or the map chunk never arrive: offline, blocked, or simply not scrolled into view yet.

Points 5 and 7 are the ones you notice in production. A map in the middle of a form on a phone is a 300px area where a swipe pans the map instead of the page, and a map that fails shows a gray box with no idea where the pin was.

## Which React map libraries can you build a location picker on?

Leaflet with OpenStreetMap needs no API key, while Google Maps and Mapbox need a key or token and an account. Facts are from each project's public docs, as of October 2026.

| Option | What it is | Key needed | Draggable marker |
|---|---|---|---|
| [react-leaflet](https://react-leaflet.js.org/) | React components for Leaflet (v5 docs), [Hippocratic License 2.1](https://github.com/PaulLeCam/react-leaflet/blob/master/LICENSE.md) | No for Leaflet itself; depends on your tile provider | Leaflet's `draggable` marker option and `dragend` event |
| [@vis.gl/react-google-maps](https://visgl.github.io/react-google-maps/) | React components and hooks for the Google Maps JavaScript API | Yes, Google says the platform [requires an API key](https://developers.google.com/maps/documentation/javascript/get-api-key) for authentication and billing | Yes, through the Maps JavaScript API |
| [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/example/drag-a-marker/) | Vector map library, used from React through wrappers | Yes, a Mapbox access token | `draggable: true` and a `dragend` listener |
| [Wingo UI Location Map](https://wingo-ui.com/components/location-map) | Pro source component on Leaflet 1.9 and OpenStreetMap tiles by default | No by default; `tileUrl` points at any raster provider | Built in, plus click to place, keyboard moves and a radius |

Google is the better choice when you need Google's place data and street-level address autocomplete worldwide, and you are fine with billing. Mapbox fits when you want vector styles. A react leaflet location picker is the lightest path to a pin on a map with no account, and that is what the Location Map is built on, so you get Leaflet's tiles without writing the click, drag, search and phone behavior yourself.

## What props does the Location Map need?

A value and an `onValueChange`. The value is a `GeoPoint` (`{ lat, lng }`) or `null` for no pin yet. The map calls `onValueChange` after a drag ends, a click places the pin, a search result or the locate button moves it, or the keyboard rests.

```bash
npx wingo-ui@latest add location-map
```

The CLI copies `components/ui/location-map.tsx`, the components and helpers it imports, and installs `leaflet`, `motion` and `lucide-react`.

> Live demo (Location Map): Click the map to drop the pin, drag it onto another building, then move the radius slider: the circle and the towns count follow. Try it at [Location Map](https://wingo-ui.com/components/location-map) and install it with `npx wingo-ui@latest add location-map`.

The built-in search is offline and covers Romanian towns, areas and counties from [ro-locations](https://wingo-ui.com/components/ro-locations), which is why the demo above sits in Cluj-Napoca. For a US app, pass a center and a geocoder that knows your country:

```tsx
"use client";

import { useState } from "react";
import {
  LocationMap,
  nominatimGeocoder,
  nominatimReverseGeocoder,
  type GeoPoint,
} from "@/components/ui/location-map";

// both helpers share one queue: at most one Nominatim request a second
const geocoder = nominatimGeocoder({ email: "dev@example.com", countryCodes: "us" });
const reverseGeocoder = nominatimReverseGeocoder({ email: "dev@example.com" });

const NEW_YORK: GeoPoint = { lat: 40.7128, lng: -74.006 };

export function DropOffLocation() {
  const [point, setPoint] = useState<GeoPoint | null>(null);

  return (
    <LocationMap
      value={point}
      onValueChange={setPoint}
      center={NEW_YORK}
      defaultZoom={12}
      geocoder={geocoder}
      reverseGeocoder={reverseGeocoder}
      showTownsInRadius={false}
      locale="en-US"
      labels={{ search: "Search an address in New York" }}
    />
  );
}
```

`nominatimGeocoder` defaults `countryCodes` to `"ro"`, so set it (or pass `""` for the whole world). The reverse geocoder names the pin with a street and town 500ms after it stops moving. Without one, the place line falls back to the nearest Romanian town, and outside Romania it shows only the coordinates.

The other props you will reach for: `defaultZoom` (13 shows a neighborhood, 16 a street), `draggable` and `clickToPlace` (both on by default), `readOnly` for a store page that only shows where the store is, `searchable` and `locate` to hide the search field or the my-location button, and `height` (320px by default).

## How do I add a radius to the map?

Pass a radius in kilometers. A radius map React apps use for delivery zones or "listings nearby" is one more controlled value: `radius` and `onRadiusChange`, with `radiusRange` and `radiusStep` for the slider under the map. `null` or `0` hides both the circle and the slider.

```tsx
"use client";

import { useState } from "react";
import { LocationMap, type GeoPoint } from "@/components/ui/location-map";
import { Button } from "@/components/ui/button";

export function DeliveryZone() {
  const [point, setPoint] = useState<GeoPoint | null>({ lat: 40.7128, lng: -74.006 });
  const [radius, setRadius] = useState<number | null>(5);

  async function save() {
    if (!point || !radius) return;
    await fetch("/api/zones", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ lat: point.lat, lng: point.lng, radiusKm: radius }),
    });
  }

  return (
    <div className="flex flex-col gap-4">
      <LocationMap
        value={point}
        onValueChange={setPoint}
        radius={radius}
        onRadiusChange={setRadius}
        radiusRange={[1, 25]}
        radiusStep={1}
        defaultZoom={11}
        showTownsInRadius={false}
        locale="en-US"
      />
      <Button tone="brand" onClick={save} disabled={!point || !radius}>
        Save zone
      </Button>
    </div>
  );
}
```

`onRadiusChange` fires on every step of the slider, and the view fits the circle when you let go. If you draw the zone yourself with plain Leaflet, remember that `L.Circle` takes meters, not kilometers. `showTownsInRadius` adds a "14 towns in range" count from the Romanian town list, so turn it off elsewhere. The [Slider](https://wingo-ui.com/components/slider) under the map is the same one you use in forms.

## How do I search for an address?

Pass a `geocoder`: a function that takes the query and `{ signal, locale }` and returns `GeocodeResult[]` (`id`, `label`, optional `description`, `point`, optional `bounds`). The map debounces typing, aborts the previous request through the signal, and fits the view to `bounds` when a result has them. The results list is the [Search Input](https://wingo-ui.com/components/search-input) with arrow keys and Enter.

`nominatimGeocoder` is a good prototype. OpenStreetMap's [Nominatim usage policy](https://operations.osmfoundation.org/policies/nominatim/) allows at most one request per second and rules out client-side autocomplete, and the helper queues requests to one a second, but a search field that queries as you type is still the use case the policy warns against. For production, point the geocoder at your own route, which can call any provider, cache the answers and keep the key on the server:

```ts
import type { GeocodeResult, Geocoder } from "@/components/ui/location-map";

export const placesGeocoder: Geocoder = async (query, { signal, locale }) => {
  const params = new URLSearchParams({ q: query, lang: locale });
  const response = await fetch(`/api/places?${params}`, { signal });
  if (!response.ok) throw new Error(`places ${response.status}`);
  return (await response.json()) as GeocodeResult[];
};
```

A geocoder that throws shows "No places found" instead of an error. If you also collect a written address, the pin and the form fields complement each other: the [Address Input](https://wingo-ui.com/components/address-input) holds the street and number, the map holds the exact spot. The [Romanian address format guide](https://wingo-ui.com/blog/romanian-address-format-react-form) shows that form part by part, and the [React combobox guide](https://wingo-ui.com/blog/react-combobox) covers the keyboard rules the results list follows.

## How does it behave on a phone?

The inline map does not take the page scroll. With `interactive="auto"` (the default) the map is live in place with a mouse, while on touch a tap opens a full-height sheet with the search on top, a map you can pinch, drag and tap, and Done at the bottom. Closing the sheet any other way discards the change, so a stray swipe never moves the pin. Inline, a phone shows at most 220px of map, since the full one is a tap away.

`interactive="always"` keeps the map live in place everywhere, and `interactive="expand"` always opens the sheet (a dialog on desktop), which suits a compact form row. The sheet is the [Drawer](https://wingo-ui.com/components/drawer); the [React bottom sheet guide](https://wingo-ui.com/blog/react-bottom-sheet-for-the-web) explains why sheets beat popovers below 768px. On desktop, `scrollWheelZoom="focus"` lets the wheel zoom only after you clicked into the map and stops when the pointer leaves, so scrolling past the map never gets stuck on it.

## Is the location picker accessible?

Yes, with a keyboard path for every move. Tab reaches the search, the map (arrows pan, plus and minus zoom), the pin (arrows move it 10px, Shift and the arrows 50px, committed a moment after you stop), the map buttons and the slider. Screen readers get the map described by the place name and coordinates under it and hear every move as "Pin moved: ..." from a live region. Every string, announcements included, lives in `labels` and merges over `DEFAULT_LOCATION_MAP_LABELS`, so you translate it without forking the file. Reduced motion drops the pin's drop animation and the map's animated flights.

## What happens when the map cannot load?

A drawn static card takes its place: a neighborhood sketch with the pin, the radius ring and a chip with the place and coordinates. You see it before the map loads (with `lazy` on, Leaflet loads only once the map is within 200px of the viewport), when the browser is offline, when more than six tiles fail in a row, and when the map chunk fails. Leaflet is imported from an effect, never at module scope, so the component renders on the server without `next/dynamic`.

`staticOnly` skips the map entirely. For an email, a print view, a thumbnail or a list of fifty stores, use `StaticMap` on its own: it needs no map library and no network.

> Live demo (Location Map): The same picker with staticOnly on: the drawn card with the pin, the radius ring, the place and the coordinates, with no tiles requested. Try it at [Location Map](https://wingo-ui.com/components/location-map) and install it with `npx wingo-ui@latest add location-map`.

```tsx
import { StaticMap } from "@/components/ui/location-map";

export function OrderMap() {
  return (
    <StaticMap
      value={{ lat: 40.7128, lng: -74.006 }}
      place="350 Broadway, New York"
      height={180}
      tone="brand"
      locale="en-US"
    />
  );
}
```

## What are the common location picker pitfalls?

Four mistakes show up once a picker leaves the demo.

- **Shipping on the public OSM tiles.** The [tile usage policy](https://operations.osmfoundation.org/policies/tiles/) has no SLA and can block heavy use. Set `tileUrl` to your provider's raster tiles and `attribution` to the credit it requires.
- **Romanian defaults left on.** Without props the map centers on Cluj-Napoca, searches Romanian places offline, counts Romanian towns in the radius and `nominatimGeocoder` limits results to `"ro"`. A US app sets `center`, `geocoder` with `countryCodes`, `reverseGeocoder` and `showTownsInRadius={false}`, as in the examples above.
- **Saving on every slider step.** `onRadiusChange` fires per step; save on submit or debounce it.
- **Dark mode without a plan.** `darkTiles="filter"` inverts the light tiles in dark mode with no second provider; `"url"` swaps to `darkTileUrl`, and `"none"` keeps light tiles in both themes.

## How do I add it to my project?

Run `npx wingo-ui@latest add location-map`. The Location Map is a Pro component ([pricing](https://wingo-ui.com/pricing)); the Search Input and Drawer it builds on are free. More single-component deep dives are in the [component guides category](https://wingo-ui.com/blog/category/components), and the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) applies the same touch, keyboard and screen reader rules to every form control.

## Components in this post

- [Location Map](https://wingo-ui.com/components/location-map) (Pro): A React location map to pick or show a place: a draggable pin, an optional radius with a towns count, offline search and a static fallback. Install: `npx wingo-ui@latest add location-map`

## FAQ

### How do I get the latitude and longitude when a user clicks a map in React?

In react-leaflet you read the click with the useMapEvents hook and store e.latlng in state. The Wingo UI Location Map does this for you: a click places the pin and onValueChange receives { lat, lng }.

### Why does Leaflet throw window is not defined in Next.js?

Leaflet reads window when the module is imported, so importing it in code that runs on the server crashes the render. Load it only in the browser, either with next/dynamic and ssr: false from a client component or with a dynamic import inside an effect, which is what the Location Map does.

### What unit does a Leaflet circle radius use?

Leaflet's L.Circle takes its radius in meters. The Location Map's radius prop is in kilometers and converts it for you.

### Can I use OpenStreetMap tiles and Nominatim in production?

Only for light use. The OSM tile policy gives no SLA and can block heavy traffic, and the Nominatim policy allows at most one request per second and no client-side autocomplete, so production apps should point tileUrl and the geocoder at their own provider.

### Is the Wingo UI Location Map free?

No, the Location Map is a Pro component. The Search Input and Drawer it builds on are free.

---

Source: https://wingo-ui.com/blog/react-location-picker
