Appearance
Markers
Markers are images or icons pinned to positions on the plan, such as devices, people, tags, alerts or points of interest. They work the same way in the 2D viewer and the 3D viewer, and in every package: Vue, React and the HTML elements take the same Marker objects.
ts
import type { Marker } from '@jbaysolutions/floorplan-vue' // or floorplan-react / floorplan-elements
const markers: Marker[] = [
{ id: 'ap-1', x: 840, y: 640, src: '/icons/wifi.png', label: 'Wi-Fi AP', color: '#6366f1', pulse: true },
{ id: 'cam-1', x: 840, y: 860, src: '/icons/camera.svg', label: 'Entrance camera' },
{ id: 'note', x: 300, y: 200, icon: '📌', label: 'Check this socket' },
]Pass them to a viewer with the markers prop (in Vue :markers="markers", in React markers={markers}, on the element the markers property).
The Marker object
| Field | Type | Description |
|---|---|---|
id | string | number | Required. Must be unique. |
x, y | number | Required. Plan coordinates in cm. |
floor | string | Id of the floor the marker is on. Leave it out for single-floor plans. |
src | string | Image URL: PNG, SVG, JPG, WebP or a data: URI. |
icon | string | Text or emoji, used when there's no src. |
label | string | Tooltip shown on hover (always shown when alwaysShowLabels). |
color | string | Accent colour (ring, pin, pulse). |
size | number | Size in px. Markers keep the same size on screen at every zoom level. |
variant | 'badge' | 'pin' | 'plain' | 'dot' | Overrides the viewer's markerVariant. |
pulse | boolean | Animated pulse ring, useful for "live" items. |
draggable | boolean | Overrides the viewer's markersDraggable. |
data | unknown | Anything you like. Returned untouched in events. |
Styles
| Variant | Looks like | Best for |
|---|---|---|
badge | Round badge with a white ring; the image fills the circle | Square or round icons (default in 2D) |
pin | Map-style teardrop; the point marks the exact spot | Exact locations (default in 3D) |
plain | Just your image with a soft shadow | Icons that already have their own shape |
dot | Small coloured dot | Dense data, heat-map-like layouts |
Set the style for every marker with the viewer's markerVariant prop, or for one marker with its variant field. Here are the house's markers drawn as pins:
Loading the live demo…
Placing a marker where the user clicks
vue
<script setup lang="ts">
import { ref } from 'vue'
import { FloorPlanViewer, type Marker, type PlanClickEvent } from '@jbaysolutions/floorplan-vue'
const markers = ref<Marker[]>([])
function place({ x, y, room, floor }: PlanClickEvent) {
markers.value.push({ id: crypto.randomUUID(), x, y, floor: floor.id, src: '/icons/tag.png', label: room?.name ?? 'Outside' })
}
</script>
<template>
<FloorPlanViewer :plan="plan" :markers="markers" @plan-click="place" />
</template>tsx
import { useState } from 'react'
import { FloorplanViewer, type Marker, type PlanClickEvent } from '@jbaysolutions/floorplan-react'
const [markers, setMarkers] = useState<Marker[]>([])
function place({ x, y, room, floor }: PlanClickEvent) {
setMarkers((ms) => [...ms, { id: crypto.randomUUID(), x, y, floor: floor.id, src: '/icons/tag.png', label: room?.name ?? 'Outside' }])
}
<FloorplanViewer plan={plan} markers={markers} onPlanClick={place} style={{ height: 500 }} />js
const viewer = document.querySelector('floorplan-viewer')
viewer.markers = []
viewer.addEventListener('plan-click', (e) => {
const { x, y, room, floor } = e.detail
viewer.markers = [...viewer.markers, { id: crypto.randomUUID(), x, y, floor: floor.id, src: '/icons/tag.png', label: room?.name ?? 'Outside' }]
})Letting users drag markers
The viewer doesn't change your data. It tells you where the marker was dropped, and you update it:
vue
<FloorPlanViewer
:plan="plan"
:markers="markers"
markers-draggable
@marker-move="({ marker, x, y }) => Object.assign(markers.find((m) => m.id === marker.id)!, { x, y })"
/>tsx
<FloorplanViewer
plan={plan}
markers={markers}
markersDraggable
onMarkerMove={({ marker, x, y, floor }) =>
setMarkers((ms) => ms.map((m) => (m.id === marker.id ? { ...m, x, y, floor: floor.id } : m)))
}
style={{ height: 500 }}
/>html
<floorplan-viewer markers-draggable style="height: 500px"></floorplan-viewer>
<script type="module">
const viewer = document.querySelector('floorplan-viewer')
viewer.addEventListener('marker-move', (e) => {
const { marker, x, y, floor } = e.detail
viewer.markers = viewer.markers.map((m) => (m.id === marker.id ? { ...m, x, y, floor: floor.id } : m))
})
</script>While dragging, the marker follows the pointer and marker-drag fires continuously. A click without movement still fires marker-click. Markers can be dragged in the 2D viewer only.
Try both here: click the plan to place a tag, then drag any marker to another room.
Loading the live demo…
Click the plan to see the event.
Custom marker templates
Draw markers your own way, for example with live values or status badges. In Vue use the marker slot, in React the renderMarker prop, and on the element the markerRenderer property (a function that returns an HTML string or a DOM node).
vue
<FloorPlanViewer :plan="plan" :markers="sensors">
<template #marker="{ marker, selected }">
<div class="sensor" :class="{ selected }">
{{ marker.data.temperature }}°C
</div>
</template>
</FloorPlanViewer>tsx
<FloorplanViewer
plan={plan}
markers={sensors}
renderMarker={(marker, { selected }) => (
<div className={selected ? 'sensor selected' : 'sensor'}>{marker.data.temperature}°C</div>
)}
style={{ height: 500 }}
/>js
viewer.markers = sensors
viewer.markerRenderer = (marker, { selected }) =>
`<div class="sensor ${selected ? 'selected' : ''}">${marker.data.temperature}°C</div>`The second argument (the slot props in Vue) is { selected, dragging, zoom } in the 2D viewer (Vue also passes marker in the slot props) and { selected, occluded } in the 3D viewer. The renderer is called again when the marker or its state changes.
Your template is positioned with its top-left corner on the marker's point. Centre it with transform: translate(-50%, -50%), or lift a pin above the point with translate(-50%, -100%).
Markers on floors
In a multi-floor building, give each marker the floor id it belongs to. The 2D viewer shows only the markers of the floor on screen. The 3D viewer shows the selected floor's markers, or every floor's markers in the exploded view. Markers without a floor belong to the ground floor.
focusMarker(id) switches to the marker's floor before flying to it.
Selecting and focusing
Highlight a marker with selectedMarkerId, and fly to one with focusMarker(id):
vue
<script setup lang="ts">
import { ref } from 'vue'
import { FloorPlanViewer } from '@jbaysolutions/floorplan-vue'
const viewer = ref<InstanceType<typeof FloorPlanViewer>>()
const selected = ref<string | number | null>(null)
const goToAccessPoint = () => viewer.value?.focusMarker('ap-1') // fly to it
</script>
<template>
<FloorPlanViewer ref="viewer" :plan="plan" :markers="markers" :selected-marker-id="selected" @marker-click="(m) => (selected = m.id)" />
</template>tsx
const viewer = useRef<FloorplanViewerElement>(null)
const [selected, setSelected] = useState<string | number | null>(null)
<FloorplanViewer ref={viewer} plan={plan} markers={markers} selectedMarkerId={selected} onMarkerClick={(m) => setSelected(m.id)} style={{ height: 500 }} />
viewer.current?.focusMarker('ap-1') // fly to itjs
viewer.addEventListener('marker-click', (e) => (viewer.selectedMarkerId = e.detail.marker.id))
viewer.focusMarker('ap-1') // fly to it