Appearance
React
@jbaysolutions/floorplan-react is the library as React components, for React 18 and 19. They're built on the HTML elements, which have Vue bundled inside: your app doesn't install or import Vue.
Every feature works the same as in the other packages: floors, markers, pools, themes, add-on packs. Plans use the same JSON format. The component pages list every prop, event and method: Editor, 2D viewer, Markers and 3D viewer.
At a glance
| In React | |
|---|---|
| Components | FloorplanEditor, FloorplanViewer, FloorplanViewer3D |
| Import from | @jbaysolutions/floorplan-react; the 3D viewer from @jbaysolutions/floorplan-react/3d |
| Stylesheet | Nothing to import: added to the page when the first component appears |
| Data in | Props in camelCase: plan, markers, theme, showGrid… |
| Events out | Callback props that receive plain values: onChange(plan), onPlanClick(e)… |
| Methods | On the ref, which is the underlying element: ref.current.focusMarker('ap-1') |
| Custom markers | renderMarker={(marker, state) => <MyPin … />} |
Install
bash
npm install @jbaysolutions/floorplan-react
npm install three # only for FloorplanViewer3D
npm install @jbaysolutions/floorplan-pack-garden # optional add-on pack@jbaysolutions/floorplan-elements is installed with it. React and React DOM 18 or newer are peer dependencies; three.js 0.160 or newer is an optional one.
There's no stylesheet to import: it's added to the page when the first component appears. To link it yourself instead, see the stylesheet on the elements page (injectStyles is exported from this package too).
A first page
tsx
import { useRef, useState } from 'react'
import {
FloorplanEditor,
FloorplanViewer,
createEmptyPlan,
type FloorPlan,
type Marker,
type FloorplanViewerElement,
} from '@jbaysolutions/floorplan-react'
export function Building() {
const [plan, setPlan] = useState<FloorPlan>(() => createEmptyPlan('Head office'))
const [markers, setMarkers] = useState<Marker[]>([])
const viewer = useRef<FloorplanViewerElement>(null)
return (
<>
<FloorplanEditor plan={plan} onChange={setPlan} style={{ height: 600 }} />
<FloorplanViewer
ref={viewer}
plan={plan}
markers={markers}
markersDraggable
style={{ height: 500 }}
onPlanClick={({ x, y, room, floor }) =>
setMarkers((ms) => [...ms, { id: crypto.randomUUID(), x, y, floor: floor.id, icon: '📍', label: room?.name ?? 'Outside' }])
}
onMarkerMove={({ marker, x, y, floor }) =>
setMarkers((ms) => ms.map((m) => (m.id === marker.id ? { ...m, x, y, floor: floor.id } : m)))
}
/>
</>
)
}Give each component a height
The components fill the box they're given. Set a height with style, className or the parent's layout.
Props
The props are the components' options in camelCase: plan, markers, floor, theme, showGrid, showFurniture, markersDraggable, selectedMarkerId, highlightRooms, mode (3D)… The editor's plan prop is plan (v-model in the Vue package).
The components also accept className, style, id, title, data-* and aria-*.
Controlled or starting value
A prop is passed on only when its value changes. What the user changes inside a component (the plan being edited, the floor shown, the 3D mode) stays as it is until you pass a different value. So you can use a prop either way:
- Controlled: keep the value in state and update it from the callback (
plan+onChange,floor+onFloorChange,mode+onModeChange). - Starting value only: pass it once and don't update it; the component carries on from there.
tsx
// controlled: the floor follows your state, and your state follows the user
const [floor, setFloor] = useState<string | null>(null)
<FloorplanViewer plan={plan} floor={floor} onFloorChange={setFloor} />
// starting value: opens on floor 'f1', then the user is free to switch
<FloorplanViewer plan={plan} floor="f1" />Removing a prop puts the component's default back.
Callbacks
Callbacks receive plain values, not DOM events:
| Component | Callback | Arguments |
|---|---|---|
FloorplanEditor | onChange | (plan) after every completed edit |
FloorplanEditor | onSelectionChange | (selection) |
| all | onFloorChange | (floorId) |
| viewers | onPlanClick | ({ x, y, room, floor, originalEvent }) |
| viewers | onMarkerClick | (marker, originalEvent) |
| viewers | onRoomHover | (room) (or null) |
FloorplanViewer | onMarkerDrag, onMarkerMove | ({ marker, x, y, room, floor }) |
FloorplanViewer3D | onModeChange | ('floor' | 'stacked' | 'exploded') |
The viewers never change your data: on onMarkerMove, update the marker in your own state.
A callback can change between renders; the component always calls the latest one.
Methods: the ref
A component's ref is the underlying element, with the same methods as the other packages:
tsx
const viewer = useRef<FloorplanViewerElement>(null)
// …
viewer.current?.focusMarker('ap-1') // fly to a marker (switches floor if needed)
viewer.current?.fit()Other examples: editor.undo(), editor.addFloor('above'), editor.getPlan(), viewer3d.setMode('exploded'), viewer3d.snapshot(). The ref types are FloorplanEditorElement, FloorplanViewerElement and FloorplanViewer3DElement (from /3d).
One rename: the 2D and 3D viewers' focus(x, y) is focusPoint(x, y) here, so the element's own focus() keeps working.
Custom markers
renderMarker draws each marker with your own React component (the marker slot in the Vue package):
tsx
<FloorplanViewer
plan={plan}
markers={markers}
renderMarker={(marker, { selected }) => <SensorBadge reading={marker.data} active={selected} />}
/>The second argument is { selected, dragging, zoom } in 2D (MarkerState) and { selected, occluded } in 3D (MarkerState3D; occluded means hidden behind a wall or slab). The markers render through portals, so they keep your React context.
Your content is anchored at the marker's point by its top-left corner. Centre it with transform: translate(-50%, -50%), or lift a pin above the point with translate(-50%, -100%).
The 3D viewer
The 3D viewer is a separate entry, so three.js only loads where it's used. Load it lazily to keep it out of the first page load:
tsx
import { Suspense, lazy } from 'react'
import type { FloorPlan, Marker } from '@jbaysolutions/floorplan-react'
const FloorplanViewer3D = lazy(() =>
import('@jbaysolutions/floorplan-react/3d').then((m) => ({ default: m.FloorplanViewer3D })),
)
export function Building3D({ plan, markers }: { plan: FloorPlan; markers: Marker[] }) {
return (
<Suspense fallback={null}>
<FloorplanViewer3D plan={plan} markers={markers} mode="stacked" style={{ height: 500 }} />
</Suspense>
)
}Add-on packs and helpers
ts
import { registerPack } from '@jbaysolutions/floorplan-react'
import { registerPack3D } from '@jbaysolutions/floorplan-react/3d'
import { garden } from '@jbaysolutions/floorplan-pack-garden'
import { garden3D } from '@jbaysolutions/floorplan-pack-garden/3d'
registerPack(garden) // once, before the components render
registerPack3D(garden3D) // only if the 3D viewer is usedThe data helpers are exported too (createEmptyPlan, normalizePlan, createFloor, formatArea, FURNITURE_CATALOG…, see Data model), plus THEMES_3D from /3d. See Add-on packs for what the packs contain.
TypeScript
Types are included: the data types (FloorPlan, Marker, PlanClickEvent, MarkerMoveEvent, Selection…), the props interfaces (FloorplanEditorProps, FloorplanViewerProps, FloorplanViewer3DProps from /3d), the marker states and the element types for refs.
Server rendering
This applies to Next.js and other server-rendering frameworks. The components are marked 'use client', so server components can render them. On the server they render an empty element; the plan appears once the page runs in the browser.