shadcn.io is not affiliated with official shadcn/ui
Location Picker Dialog Block for Radix UI
A courier's pickup dialog shown open: a search field with Saved, Recent and Nearby places in an inline list, a drawn street map with a pin you drop or move with the arrow keys, and a latitude and longitude readout.
Installation
Overview
Let people say where a parcel is collected by pointing at it, with this location picker dialog for React with Radix UI. It opens from the Book a pickup page of Parcelmoor, a same-day courier: the page's header holds an outline Pick on map Button, and the booking form, a Pickup field, a Drop-off field and a row of time chips, is drawn under a soft scrim inside a hairline frame with a plus mark on each corner. The dialog has a search Input with its suggestions in an inline Listbox, grouped as Saved, Recent and Nearby, and beside it (above the list on a phone) a street map drawn in SVG with a river, a street grid and two parks. Press the map to drop the pin, or focus it and move the pin with the arrow keys, 8 units a step and 32 with Shift; choosing a place moves the pin to it. A readout works out the latitude and longitude and names the nearest place. Confirm pickup, the only Shine Button, writes the place and its coordinates into the Pickup field. For preview the dialog is drawn open in place when the page loads; the header button opens the real Dialog, with the same search, pin and list. For couriers, delivery, repair and rental apps, and any form that needs a place.
FAQ
Why is the dialog already open, and what do I keep?
So you can see it without a click. On load the dialog is a copy drawn in place over the booking form: a native dialog element that isn't modal, so nothing is trapped, hidden or focused, and the page around the preview keeps working. The Pick on map button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same PickerFields component and share the block's state, so a pin dropped in the copy is where it sits in the real one. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.
How do I place the pin without a mouse?
The map is one focusable control named Street map. With it focused the arrow keys move the pin 8 units, and Shift with an arrow moves it 32; the pin stays inside the map. Pressing the map with a pointer drops the pin under it, and Enter or Space on the focused map do nothing so a keyboard user never drops it by accident. Choosing a place in the list, with the mouse, or with the arrow keys and Enter after a search, is the other route, and Enter in the search field takes the top match. Moving the pin by keys is not announced on every step; the readout under the map shows the coordinates, and the polite message speaks when you drop the pin or pick a place.
Where do the coordinates come from?
They are worked out, not looked up. The map has a typed grid: the top start corner is 51.5200, -0.1140 and one map unit is 0.00007 degrees of latitude and 0.00009 of longitude, so the pin's position gives its latitude and longitude, shown to four decimals. The nearest place is the typed place with the smallest distance to the pin. The streets and places are invented, and the grid constants and PLACES are at the top of the file; in your app replace them with a geocoder and a map library, and read the pin from its click event.
Why is there no Use my location button?
It would need the browser's Geolocation permission, and a demo should not ask for it. Nothing here reads the device. If you add it, call navigator.geolocation.getCurrentPosition from the button's click handler (never on load), handle a refusal in words, and move the pin to the result the same way a chosen place does.
Is the list accessible from the keyboard?
Yes. The search field is labelled, the list is a listbox named Places, and the rows are options with a check on the chosen one. Down arrow in the search field moves into the list. The real dialog is named by its title and described by its sentence, focus starts in the search and stays inside until the dialog closes, and Escape, Cancel and the close button return focus to Pick on map. The trigger is the button wrapped in DialogTrigger with asChild, and the dialog is controlled with open and onOpenChange. The copy shown on load moves no focus; Escape inside it, Cancel and its close button close it and put focus on the button.
What stays local in the demo, and how do I wire it up?
All of it. Nothing is booked, searched or sent: the places are typed, the search filters them in the tab, and the only thing that leaves the dialog is the place and coordinates that Confirm pickup writes into the drawn Pickup field, with a polite message saying nothing was booked. In your app give confirm your own write-back (a form value, a booking request) and feed the list from your geocoder.
How does it look on a phone?
The copy shown on load docks to the foot of the page as a bottom sheet: the search first, then the map at its full width, then the list in a box that scrolls on its own, with the Cancel and Confirm pickup buttons pinned below. The list keeps one height whatever you type, and an empty search shows a message in the same box. The real dialog keeps the Dialog's own phone layout: a sheet stuck to the bottom of the screen, scrolling inside.