stx components
On this page 8
@ts-maps/stx gives stx apps the same map components the other bindings have.
<Map:center="34.02-118.47:zoom="14theme="
basemap="dark" tiles="{{ tileUrl }}">
<NavigationControlposition="
<GeocoderControl:options="placeholder: ''
<Marker:lat="34.02:lng="-118.47
<PopupOcean Park</Popup
</Marker
</Map
That is the whole page — no client script. The components render markup on the server; the map builds itself from it on mount.
Install
bun add @ts-maps/stx ts-maps
Register the plugin so the components resolve by tag name, and link the stylesheet from your layout:
// stx.config.ts
export default {
plugins: ['@ts-maps/stx/stx-plugin'],
}
<linkrel=""href=""
The stylesheet is exported as @ts-maps/stx/styles.css if your build collects
CSS through imports. Without it the map's panes fall back to position: static
and stack down the page instead of overlaying — a blank-looking map with the
tiles somewhere below the fold.
Components
Map | The map, and the container everything else nests inside |
TileLayer | Raster tiles |
Source / Layer | Style-spec sources and the layers that draw them |
Marker | A pin, default or your own markup |
Popup | A bubble, bound to a marker or free-standing |
ZoomControl NavigationControl GeocoderControl FullscreenControl LocateControl ScaleControl AttributionControl | Map controls |
TurnByTurn | Turn-by-turn navigation; events arrive as turnbyturn:* DOM events |
OfflineMaps | Offline maps; events arrive as offlinemaps:* DOM events |
Search | Search; events arrive as search:* DOM events, and Directions uses a TurnByTurn in the same map |
Same names and prop shapes as the React, Vue, Svelte and Solid bindings.
LayersControl is deliberately not a component: it takes dictionaries of live
layer instances rather than plain data. Use the map directly for that one.
<Map>
center zoom minZoom maxZoom bearing pitch — the camera.
theme — 'light', 'dark' or 'auto', for the map's own chrome.
basemap + tiles — build one of the bundled basemaps without composing a
style yourself. basemapMode picks 'vector' (default) or 'raster';
tilesAttribution is passed through to the attribution control.
styleSpec — a full style object or a URL, when you want your own.
className, containerStyle — the container. Give it a height.
Write
className, neverclass. stx seeds every prop into the client scope as a variable, andclassis a reserved word — the generated script then fails to parse withUnexpected token 'class'.
<Marker> and <Popup>
<Marker
:lat="34.02" :lng="-118.47"
:html="'<span class=\'pin\'>🔥</span>'"
:iconSize="[46, 46]" :iconAnchor="[23, 23]"
>
<Popup:closeButton="false:open="trueStructure fire</Popup
</Marker
html swaps the default pin for your own markup. A <Popup> inside a marker
binds to it and opens on click; one with its own lat/lng stands alone.
Clicks dispatch a bubbling marker:click DOM event (rename it with
clickEvent), because stx passes props as data and a callback cannot cross the
component boundary. Listen once on an ancestor:
onMount(() => {
useEventListener(mapEl.value, 'marker:click', (e) => {
console.log(e.detail.marker.getLatLng())
})
})
Reaching the map
import { findMap, onMapEvent } from '@ts-maps/stx'
onMount(() => {
const map = findMap(el.value)
map.flyTo([34.02, -118.47], 16)
onDestroy(onMapEvent(el.value, 'moveend', () => console.log(map.getCenter())))
})
findMap walks up to the nearest <Map>, so two maps on a page each answer
for their own children.
How this differs from the other bindings
React, Vue, Svelte and Solid give a child component its own instance and its
own lifecycle, so <Marker> creates a marker for itself. stx does not work
that way: a component's <script client> is emitted once per definition,
not per use. Ten <Marker> tags produce ten pieces of markup and one script —
so a marker that builds itself yields exactly one marker however many you
write.
So children here render inert markup carrying data- attributes, and <Map>
walks its subtree once on mount and builds what it finds. Two consequences:
- Children are read at mount. Markers added to the DOM later are not picked up; add those through the map itself.
- Nesting is the wiring. There is no context to thread and no ids to match.
Two further stx behaviours the components work around, noted here because they bite anyone writing a component of their own:
- A client script with imports is bundled, and a bundled script sees none
of the server scope —
{{ value }}interpolation does not happen either. Pass data on adata-attribute instead. - stx materialises a
constonly for props the caller actually passed, so a default written in the template ($props.pitch ?? 0) evaluates toundefinedfor anything omitted. Defaults belong in the TypeScript that reads the props; seemapOptionsFrom.
Mobile
The components are ordinary DOM, so they work anywhere stx does — including a Capacitor build. Nothing here is web-only beyond the map itself, which is a canvas.
Example
playground/incident-map in the repository has the same screen twice: /
builds everything imperatively in one client script, /components uses these
components and has no client script at all.