Elements · Knowledge
Map
A location answer: pins, a route between them, and the list they came from.
Installation
npx shadcn@latest add "@assistant-ui/elements-map-answer"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 initThen 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.
npx shadcn@latest add "@assistant-ui/elements-map-answer"Props-driven: no runtime or provider required.
MapAnswer lays location results on a schematic grid: pins you can select, an optional route between them, and the list they came from underneath. With a runtime the pins come from a tool call that looked places up; standalone you already hold them.
Getting started
assistant-ui has no map or geocoding concept, and MapAnswer draws no real tiles or projection: x and y are plain 0 to 100 percentages that place a pin on the grid, not latitude and longitude. Whatever looks places up is responsible for turning real coordinates into that percentage layout before the result reaches this component.
Render the tool call
"use client";
import { useState } from "react";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import {
MapAnswer,
type MapPin,
} from "@/components/assistant-ui/elements/map-answer";
type FindPlacesResult = {
pins: MapPin[];
route: boolean;
};
export const FindPlacesToolUI: ToolCallMessagePartComponent<
{ query: string },
FindPlacesResult
> = ({ result }) => {
const [activeId, setActiveId] = useState("");
if (!result) return null;
const activePin = activeId || (result.pins[0]?.id ?? "");
return (
<MapAnswer
pins={result.pins}
activeId={activePin}
route={result.route}
onSelect={setActiveId}
/>
);
};Register the tool
import { defineToolkit } from "@assistant-ui/react";
import { z } from "zod";
import { FindPlacesToolUI } from "@/components/assistant-ui/elements/find-places-tool-ui";
export const toolkit = defineToolkit({
find_places: {
type: "frontend",
description: "Find places for a query and lay them out on a schematic map.",
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => lookupPlaces(query),
render: FindPlacesToolUI,
},
});lookupPlaces owns the coordinate math: normalize each result's real latitude and longitude against the bounding box of the whole set, so the closest pins end up close together on the grid. Wire the toolkit in with Tools({ toolkit }); see Tool UI.
Standalone, you already hold the pins, their layout percentages, and which one is active.
Hold the active pin
"use client";
import { useState } from "react";
import {
MapAnswer,
type MapPin,
} from "@/components/assistant-ui/elements/map-answer";
const PINS: MapPin[] = [
{ id: "a", label: "Ferry Building", detail: "0.4 mi", x: 28, y: 62 },
{ id: "b", label: "Coit Tower", detail: "1.1 mi", x: 55, y: 20 },
{ id: "c", label: "Chinatown", detail: "0.8 mi", x: 40, y: 45 },
];
export function Places() {
const [activeId, setActiveId] = useState(PINS[0].id);
return (
<MapAnswer pins={PINS} activeId={activeId} route onSelect={setActiveId} />
);
}Replace the pin set
A new search swaps the whole array and resets which pin is active.
async function search(query: string) {
const places = await fetchPlaces(query);
setPins(places);
setActiveId(places[0]?.id ?? "");
}Anatomy
<div data-slot="map-answer">
<div>
<svg>{/* hairline grid, plus an optional dashed route through pins in array order */}</svg>
{/* one marker button per pin, positioned by x/y percentage */}
</div>
<div>
{/* one row per pin, top-bordered, including the first */}
</div>
</div>The route line draws only when route is true and there are at least two pins. It is a static dashed polyline connecting pins strictly in array order, not a computed path, so reordering pins changes the drawn route without adding or removing a single pin. x and y place a marker by percentage inside a 0 to 100 viewBox; nothing clamps an out-of-range value. The active pin's marker grows and switches to a blue fill; every other marker stays a smaller, neutral fill, both sharing the same background-colored ring so they read clearly against the grid lines. Clicking a marker and clicking its row in the list below both call onSelect with the same pin.id, so either one moves the active pin. Every list row carries a top divider, including the first, since the list sits directly under the grid rather than under another row. An empty pins array renders an empty grid, its hairlines still visible, with an empty list underneath.
Examples
Draw the route
The dashed line is decorative connectivity, not a routed path: it always runs in pins order and never adjusts itself to the active pin.
<MapAnswer pins={pins} activeId={activeId} route onSelect={onSelect} />Restyle the map
Both lanes take className on the root. The grid background reads the shared field token, the card itself paper, and the list's detail column mono, all from surfaces.tsx.
<MapAnswer className="max-w-none" /* ... */ />API reference
Tool-call render props
| Prop | Type | Description |
|---|---|---|
args | TArgs | Parsed arguments. Partial while the model is still streaming them. |
argsText | string | Raw JSON argument text streamed by the model. |
result | TResult | undefined | The tool's return value once it completes. undefined while running. |
status | ToolCallMessagePartStatus | status.type is "running", "requires-action", "complete", or "incomplete". |
toolName | string | Name of the tool the model called. |
toolCallId | string | Stable id for this invocation. |
isError | boolean | undefined | Whether result represents a tool execution error. |
Register the renderer on a toolkit entry's render field and attach the toolkit with Tools({ toolkit }). See Tool UI for the full render-prop surface, including addResult, human tools, and approval gates.
MapAnswer
| Prop | Type | Default | Description |
|---|---|---|---|
pins | readonly MapPin[] | required | The markers to place, in order. Also the order the route line connects them in. |
activeId | string | required | Id of the highlighted pin. An id matching no pin highlights none. |
route | boolean | Draws a dashed line through pins in array order. No line below two pins even when true. | |
onSelect | (id: string) => void | Called with a pin's id from either its marker or its list row. | |
className | string | Merged onto the root. |
MapPin
| Field | Type | Description |
|---|---|---|
id | string | |
label | string | Marker aria-label and the list row's primary text. |
detail | string | Secondary text in the list row, for example a distance. |
x | number | Horizontal position, 0 to 100 percent. Not longitude. |
y | number | Vertical position, 0 to 100 percent. Not latitude. |
All other div props are forwarded to the root.