Skip to content

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
ComponentsFloorplanEditor, FloorplanViewer, FloorplanViewer3D
Import from@jbaysolutions/floorplan-react; the 3D viewer from @jbaysolutions/floorplan-react/3d
StylesheetNothing to import: added to the page when the first component appears
Data inProps in camelCase: plan, markers, theme, showGrid…
Events outCallback props that receive plain values: onChange(plan), onPlanClick(e)…
MethodsOn the ref, which is the underlying element: ref.current.focusMarker('ap-1')
Custom markersrenderMarker={(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:

ComponentCallbackArguments
FloorplanEditoronChange(plan) after every completed edit
FloorplanEditoronSelectionChange(selection)
allonFloorChange(floorId)
viewersonPlanClick({ x, y, room, floor, originalEvent })
viewersonMarkerClick(marker, originalEvent)
viewersonRoomHover(room) (or null)
FloorplanVieweronMarkerDrag, onMarkerMove({ marker, x, y, room, floor })
FloorplanViewer3DonModeChange('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 used

The 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.

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