Skip to content

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 ​

FieldTypeDescription
idstring | numberRequired. Must be unique.
x, ynumberRequired. Plan coordinates in cm.
floorstringId of the floor the marker is on. Leave it out for single-floor plans.
srcstringImage URL: PNG, SVG, JPG, WebP or a data: URI.
iconstringText or emoji, used when there's no src.
labelstringTooltip shown on hover (always shown when alwaysShowLabels).
colorstringAccent colour (ring, pin, pulse).
sizenumberSize in px. Markers keep the same size on screen at every zoom level.
variant'badge' | 'pin' | 'plain' | 'dot'Overrides the viewer's markerVariant.
pulsebooleanAnimated pulse ring, useful for "live" items.
draggablebooleanOverrides the viewer's markersDraggable.
dataunknownAnything you like. Returned untouched in events.

Styles ​

VariantLooks likeBest for
badgeRound badge with a white ring; the image fills the circleSquare or round icons (default in 2D)
pinMap-style teardrop; the point marks the exact spotExact locations (default in 3D)
plainJust your image with a soft shadowIcons that already have their own shape
dotSmall coloured dotDense 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 it
js
viewer.addEventListener('marker-click', (e) => (viewer.selectedMarkerId = e.detail.marker.id))

viewer.focusMarker('ap-1') // fly to it

JBay Floorplan is free for open source and companies under €1M yearly revenue. Licensing and pricing