Elements

Geo map

Places and routes on a real tiled map, with an accessible list of every place.

Loading map
fig. 01

Installation

npx shadcn@latest add "@assistant-ui/elements-geo-map"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

GeoMap gives a tool result a real geographic frame. It loads Leaflet only in the browser, keeps the location list available to keyboard and screen reader users, and leaves MapAnswer as the dependency free schematic option when coordinates are not the answer.

GeoMap imports leaflet/dist/leaflet.css itself; a Pages Router app imports that stylesheet in pages/_app instead.

Getting started

A backend place search returns { places, routes }, then the tool renderer passes those coordinates directly to GeoMap. The map is ready once the result arrives, while the rest of the message can explain why these places matter.

Render the backend result

components/assistant-ui/elements/find-places-tool-ui.tsx
"use client";

import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import {
  GeoMap,
  type GeoMapPlace,
  type GeoMapRoute,
} from "@/components/assistant-ui/elements/geo-map";

type FindPlacesResult = {
  places: GeoMapPlace[];
  routes: GeoMapRoute[];
};

export const FindPlacesToolUI: ToolCallMessagePartComponent<
  { query: string },
  FindPlacesResult
> = ({ result }) => {
  if (!result) return null;
  return <GeoMap places={result.places} routes={result.routes} />;
};

Return coordinates from the tool

The backend tool should return latitude and longitude in WGS 84 order, { lat, lng } for each place and [lat, lng] for every route point. GeoMap ignores nonfinite values and values beyond geographic bounds before it draws or lists them.

return {
  places: await findPlaces(query),
  routes: await findWalkingRoutes(query),
};

Anatomy

<div data-slot="geo-map">
  <div role="region" aria-label="Map of 3 places" />
  <ol>
    <li><button aria-current="true">Place label, detail, and coordinates</button></li>
  </ol>
</div>

The paper card contains printed map matter, not another bordered panel. Leaflet owns keyboard zoom after the map receives focus. Every place has a matching list button below it, where the current place is announced with aria-current; selecting either the marker or button pans to that place. Route lines and map bounds use every valid coordinate.

Examples

Draw a route

Routes are supplied paths, not directions GeoMap computes. A route with fewer than two valid points does not draw.

<GeoMap
  places={places}
  routes={[
    {
      id: "morning-walk",
      label: "Morning walk",
      points: [
        [40.7412, -73.9896],
        [40.7398, -73.9911],
      ],
    },
  ]}
/>

Use custom tiles

Without tileUrl, the map draws the public OpenStreetMap tiles in the page's ink: grayscale on a light page, inverted on a dark one. tileUrl and attribution point it at your own tile provider instead, and custom tiles are shown exactly as the provider draws them.

<GeoMap
  places={places}
  tileUrl="https://tiles.example.com/{z}/{x}/{y}.png"
  attribution="© Example Maps"
/>

The public OpenStreetMap tile servers are meant for light use under the OpenStreetMap tile usage policy, which rules out an app sending all of its traffic there. For production, set tileUrl to a provider you have an account with, and keep its attribution visible. Both props are application configuration: Leaflet renders attribution as HTML, so never fill either one from tool or model output.

Control the selection

Pass selectedId when another part of the answer needs to choose the current place too.

const [selectedId, setSelectedId] = useState(places[0]?.id);

<GeoMap
  places={places}
  routes={routes}
  selectedId={selectedId}
  onSelect={setSelectedId}
/>

API reference

Tool call render props

PropTypeDescription
argsTArgsParsed tool arguments, partial while they stream.
result{ places: GeoMapPlace[]; routes: GeoMapRoute[] } | undefinedThe backend result once the tool completes.
statusToolCallMessagePartStatusThe tool lifecycle state.
toolNamestringThe backend tool name.

Register the renderer on a toolkit entry's render field. See Tool UI for the complete render prop surface.