Location Map React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
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.
With a mouse the map is live in place: drag to pan, double-click or use the round + and − buttons to zoom, and the wheel zooms only after you clicked into the map, so scrolling the page never gets stuck on it. Click anywhere to drop the pin there (it falls in with a small squash and a ripple) or drag the pin itself; it lifts while you hold it and the circle follows. Type in the search field to find a town, an area or a county offline (an online geocoder finds street addresses); arrows move through the results, Enter picks one and the map flies there. The locate button asks for your position and moves the pin; a refusal is shown under the map. With a radius set, the slider under the map resizes the circle live, shows how many towns fall inside, and fits the view to the circle when you let go. On a phone the inline map does not steal the page scroll: tap it and a full-height sheet opens with the search on top, a fully interactive map (pinch, drag, tap to place) and Done at the bottom; closing the sheet any other way discards the changes. Keyboard: Tab reaches the search, the map (arrows pan, + and − zoom), the pin (arrows move it 10px, Shift+arrows 50px, committed a moment after you stop), the map buttons and the slider. Screen readers get the map as an application described by the place name and coordinates under it, and hear every move. Without network or before the map loads, a drawn static card shows the pin, the place and the coordinates.
Location Map is a Pro React component in the React media components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add location-map, together with Button, Drawer, Listbox, Search Input, Skeleton and 11 more and the npm packages leaflet, motion and 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 location-mapLocation Map is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import LocationMap from @/components/ui/location-map.
import { useState } from "react"
import { LocationMap, type GeoPoint } from "@/components/ui/location-map"
export function ListingLocation() {
const [point, setPoint] = useState<GeoPoint | null>({ lat: 46.77, lng: 23.5857 })
const [radius, setRadius] = useState<number | null>(5)
return (
<LocationMap
value={point}
onValueChange={setPoint}
radius={radius}
onRadiusChange={setRadius}
locale="en-US"
labels={{
search: "Search a town",
towns: (count, formatted) => (count === 1 ? "1 town nearby" : `${formatted} towns nearby`),
}}
/>
)
}Props
valueGeoPoint | nullThe pin, controlled: { lat, lng }. null shows no pin until one is placed.
defaultValuedefault"null"GeoPoint | nullThe pin while uncontrolled. The demo starts at Str. Memorandumului 28, Cluj-Napoca.
onValueChange(value: GeoPoint | null) => voidAfter a drag ends, a click places the pin, a search result or the locate button moves it, or the keyboard rests.
radiusnumber | nullThe radius in km, controlled. null or 0 hides the circle and the slider.
defaultRadiusdefault0number | nullThe radius in km while uncontrolled; 0 (or null) means no circle and no slider.
onRadiusChange(radius: number | null) => voidEvery step of the slider.
radiusRangedefault"[1, 50]"LocationMapRange ([min, max] in km)The slider's bounds in km.
radiusStepdefault1numberThe slider's step in km.
showRadiusSliderdefaulttruebooleanThe slider row under the map, while a radius is set and the map is not read-only.
showTownsInRadiusdefaulttruebooleanAdds "14 towns in range" to the radius row, counted from ro-locations (0 outside Romania).
centerGeoPointThe first view, and a new view when it changes. Defaults to the value, then Cluj-Napoca (46.7712, 23.6236).
zoomnumberThe zoom level, controlled (1 to 19). The inline map and the sheet share it.
defaultZoomdefault13numberThe zoom level while uncontrolled: 13 shows a neighbourhood, 10 a county, 16 a street.
onZoomChange(zoom: number) => voidAfter every zoom, from the buttons, the wheel, a pinch or a flight.
draggabledefaulttruebooleanThe pin can be dragged, and moved with the arrow keys once focused.
clickToPlacedefaulttruebooleanA click or a tap on the map moves the pin there.
readOnlydefaultfalsebooleanShows the pin and the circle only: no search, no slider, no locate, the pin stays put. The map still pans and zooms.
searchabledefaulttruebooleanThe search field above the map.
geocoderdefault"roLocationsGeocoder"(query: string, options: { signal: AbortSignal; locale: string }) => Promise<GeocodeResult[]>What the search asks. roLocationsGeocoder finds Romanian towns, areas and counties offline; nominatimGeocoder({ email }) finds street addresses through OpenStreetMap (one request a second, debounced 700ms). The previous request is aborted.
reverseGeocoder(point: GeoPoint, options: { signal: AbortSignal; locale: string }) => Promise<GeocodeResult | null>Names the pin after it moves ("Str. Memorandumului 28, Cluj-Napoca"), 500ms after it rests. Without it the nearest town from ro-locations names it. nominatimReverseGeocoder({ email }) is ready to use.
locatedefaulttruebooleanThe my-location button. Hidden where the browser has no geolocation; a refusal is shown and announced.
heightdefault320numberThe map's height in px. Phones show at most 220px inline, since the full map is a tap away.
tileUrldefault"https://tile.openstreetmap.org/{z}/{x}/{y}.png"stringThe raster tiles. OpenStreetMap's public tiles are for light use; a production app should point this at its own tile provider.
attributiondefault"© OpenStreetMap"stringThe credit in the corner that the tile provider requires. Linked to the OpenStreetMap copyright page for OSM tiles.
darkTilesdefault"filter""filter" | "url" | "none"Dark mode tiles. filter inverts the light tiles with a hue rotation (no second provider needed), url swaps to darkTileUrl, none keeps light tiles in both themes.
darkTileUrldefault""stringThe tiles for dark mode when darkTiles is "url", e.g. a dark raster style of your provider.
lazydefaulttruebooleanLoads Leaflet (42 KB) only once the map is within 200px of the viewport. Until then the static card shows.
staticOnlydefaultfalsebooleanNever loads the map: the drawn static card with the pin, the place and the coordinates. For emails, prints, thumbnails and long lists.
interactivedefault"auto""auto" | "always" | "expand"auto: live in place with a mouse, tap to open a full-height sheet on touch (the page keeps scrolling over the inline map). always: live in place everywhere. expand: a click or tap always opens the sheet (a dialog on desktop).
scrollWheelZoomdefault"focus""focus" | "always" | "never"focus: the wheel zooms only after the map was clicked or focused, and stops when the pointer leaves. always and never do what they say.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The pin, the circle, the slider and the sheet's Done button.
colordefault""stringAny CSS color; replaces the tone for the pin, the circle and the slider.
localedefault""stringCoordinates, distances and the geocoder's language. Empty uses the app's locale, then the browser's.
setApi(api: LocationMapApi) => voidCalled once with { flyTo(point, zoom?), fitRadius(), locate(): Promise<GeoPoint | null>, getMap(): L.Map | null }. The calls go to the sheet's map while it is open.
labelsPartial<LocationMapLabels>Every string, English by default (DEFAULT_LOCATION_MAP_LABELS): search, noResults, locate, locating, locateDenied, locateFailed, zoomIn, zoomOut, expand, done, radius(km, formatted), radiusLabel, towns(count, formatted), pin, map, sheetTitle, offline, placePin, tapToOpen, near(town, distance), coordinates(lat, lng), moved(place).
classNamesPartial<Record<"search" | "map" | "canvas" | "controls" | "control" | "attribution" | "hint" | "radius" | "slider" | "radiusLabel" | "summary" | "error" | "static" | "sheet", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last.
Parts
StaticMap
componentThe fallback card alone, no map library and no network: a drawn neighbourhood with the pin, an optional radius ring and a chip with the place and coordinates. Props: { value?: GeoPoint | null; place?: ReactNode; radius?: number | null; offline?: boolean; height?: number; tone?: Tone; color?: string; locale?: string; labels?: Partial<LocationMapLabels> }.
roLocationsGeocoder
constantThe offline default: Romanian cities, areas and counties from ro-locations, diacritics ignored. Type: Geocoder.
nominatimGeocoder
functionOpenStreetMap address search. countryCodes defaults to "ro", endpoint to nominatim.openstreetmap.org; requests wait for their turn so there is at most one a second. Signature: (options?: { email?: string; countryCodes?: string; endpoint?: string; limit?: number }) => Geocoder.
nominatimReverseGeocoder
functionThe street and town under a point, "Strada Memorandumului 28, Cluj-Napoca", in the map's locale. Signature: (options?: { email?: string; endpoint?: string }) => ReverseGeocoder.
DEFAULT_LOCATION_MAP_LABELS
constantEvery English string of the map: search, locate, zoom, radius and towns count, the sheet, offline and pin announcements. Spread it and override what you translate.
Examples
Nearby filter
A 25 km radius around Bucharest; the towns inside come from citiesWithin and follow the slider.
Online address search
nominatimGeocoder finds street addresses (one request a second), the reverse geocoder names the pin.
Read-only store location
readOnly shows the pin only; a reverseGeocoder turns the point into an address line.
Dark tiles
Switch to dark mode: "filter" inverts the light tiles, "none" keeps them light.
Tap to expand
interactive="expand": the map opens in a full-height sheet (a dialog on desktop) and Done commits the pin.
Static card in an email
StaticMap needs no map library and no network: emails, prints and long lists.
Tones and colors
The pin and the radius take any tone or CSS color.
Dependencies
Files
- components/ui/location-map.tsx
FAQ
Is the React Location Map component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Location Map?
Run npx wingo-ui@latest add location-map. It also adds the Wingo UI items it builds on (Button, Drawer, Listbox, Search Input, Skeleton and 11 more) and the npm packages leaflet, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/location-map.
How does Location Map behave on phones?
On a phone the inline map does not steal the page scroll: tap it and a full-height sheet opens with the search on top, a fully interactive map (pinch, drag, tap to place) and Done at the bottom; closing the sheet any other way discards the changes.
See also
Used in
More React media components
Articles using this component
- Nextjs Real Estate Template: Search, Map and Mortgage
Build it: screens and apps10 min read
- Nextjs Restaurant Template: Open Now, Menu, Book a Table
Build it: screens and apps10 min read
- React Location Picker: Draggable Pin, Radius, Search
Component guides9 min read