How to Build an Interactive Floor Plan with JavaScript Canvas
Interactive floor plans and building maps are used for real estate, facility management, and wayfinding applications. Konva.js makes it easy to draw complex polygonal shapes with hover detection, tooltips, and custom styling per section.
Instructions: hover over sections of the building to see its description.
- Vanilla
- React
- Vue
import Konva from 'konva'; function getData() { return { '1st Floor': { color: 'blue', points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274], }, '2nd Floor': { color: 'red', points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171], }, '3rd Floor': { color: 'yellow', points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93], }, Gym: { color: 'green', points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283], }, }; } function updateTooltip(tooltip, x, y, text) { tooltip.getText().text(text); tooltip.position({ x: x, y: y, }); tooltip.show(); } const stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: window.innerHeight, }); // add background image first const imageLayer = new Konva.Layer(); stage.add(imageLayer); Konva.Image.fromURL('https://konvajs.org/assets/line-building.png', function (bgImage) { bgImage.setAttrs({ x: 1, y: 0, }); imageLayer.add(bgImage); }); const shapesLayer = new Konva.Layer(); const tooltipLayer = new Konva.Layer(); const tooltip = new Konva.Label({ opacity: 0.75, visible: false, listening: false, }); tooltip.add( new Konva.Tag({ fill: 'black', pointerDirection: 'down', pointerWidth: 10, pointerHeight: 10, lineJoin: 'round', shadowColor: 'black', shadowBlur: 10, shadowOffsetX: 10, shadowOffsetY: 10, shadowOpacity: 0.5, }) ); tooltip.add( new Konva.Text({ text: '', fontFamily: 'Calibri', fontSize: 18, padding: 5, fill: 'white', }) ); tooltipLayer.add(tooltip); // get areas data const areas = getData(); // draw areas for (const key in areas) { const area = areas[key]; const points = area.points; const shape = new Konva.Line({ points: points, fill: area.color, opacity: 0, closed: true, name: 'area', // custom attr key: key, }); shapesLayer.add(shape); } // add layers in correct order stage.add(shapesLayer); stage.add(tooltipLayer); stage.on('mouseover', (evt) => { const shape = evt.target; if (shape && shape.name() === 'area') { // only change opacity if it's an area shape shape.opacity(0.5); } }); stage.on('mouseout', (evt) => { const shape = evt.target; if (shape && shape.name() === 'area') { // only change opacity if it's an area shape shape.opacity(0); tooltip.hide(); } }); stage.on('mousemove', (evt) => { const shape = evt.target; if (shape && shape.name() === 'area') { // only change opacity if it's an area shape const mousePos = stage.getPointerPosition(); const x = mousePos.x; const y = mousePos.y - 5; updateTooltip(tooltip, x, y, shape.getAttr('key')); } });
import { Stage, Layer, Line, Label, Tag, Text, Image } from 'react-konva'; import { useState } from 'react'; import useImage from 'use-image'; const getData = () => ({ '1st Floor': { color: 'blue', points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274], }, '2nd Floor': { color: 'red', points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171], }, '3rd Floor': { color: 'yellow', points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93], }, Gym: { color: 'green', points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283], }, }); const App = () => { const [tooltipProps, setTooltipProps] = useState({ visible: false, x: 0, y: 0, text: '', }); const [hoveredArea, setHoveredArea] = useState(null); const [backgroundImage] = useImage('https://konvajs.org/assets/line-building.png'); const areas = getData(); const handleMouseOver = (key) => { setHoveredArea(key); }; const handleMouseOut = () => { setHoveredArea(null); setTooltipProps(prev => ({ ...prev, visible: false })); }; const handleMouseMove = (e, key) => { const stage = e.target.getStage(); const mousePos = stage.getPointerPosition(); setTooltipProps({ visible: true, x: mousePos.x, y: mousePos.y - 5, text: key, }); }; return ( <Stage width={window.innerWidth} height={window.innerHeight}> <Layer> <Image x={1} y={0} image={backgroundImage} /> </Layer> <Layer> {Object.entries(areas).map(([key, area]) => ( <Line key={key} points={area.points} fill={area.color} opacity={hoveredArea === key ? 0.5 : 0} closed={true} onMouseOver={() => handleMouseOver(key)} onMouseOut={handleMouseOut} onMouseMove={(e) => handleMouseMove(e, key)} /> ))} </Layer> <Layer> <Label x={tooltipProps.x} y={tooltipProps.y} opacity={0.75} visible={tooltipProps.visible} > <Tag fill="black" pointerDirection="down" pointerWidth={10} pointerHeight={10} lineJoin="round" shadowColor="black" shadowBlur={10} shadowOffsetX={10} shadowOffsetY={10} shadowOpacity={0.5} /> <Text text={tooltipProps.text} fontFamily="Calibri" fontSize={18} padding={5} fill="white" /> </Label> </Layer> </Stage> ); }; export default App;
<template> <v-stage :config="stageSize"> <v-layer> <v-image v-if="backgroundImage" :config="{ x: 1, y: 0, image: backgroundImage }" /> </v-layer> <v-layer> <v-line v-for="(area, key) in areas" :key="key" :config="{ points: area.points, fill: area.color, opacity: hoveredArea === key ? 0.5 : 0, closed: true }" @mouseover="handleMouseOver(key)" @mouseout="handleMouseOut" @mousemove="(e) => handleMouseMove(e, key)" /> </v-layer> <v-layer> <v-label :config="{ x: tooltipProps.x, y: tooltipProps.y, opacity: 0.75, visible: tooltipProps.visible }" > <v-tag :config="{ fill: 'black', pointerDirection: 'down', pointerWidth: 10, pointerHeight: 10, lineJoin: 'round', shadowColor: 'black', shadowBlur: 10, shadowOffsetX: 10, shadowOffsetY: 10, shadowOpacity: 0.5 }" /> <v-text :config="{ text: tooltipProps.text, fontFamily: 'Calibri', fontSize: 18, padding: 5, fill: 'white' }" /> </v-label> </v-layer> </v-stage> </template> <script setup> import { ref } from 'vue'; import { useImage } from 'vue-konva'; const stageSize = { width: window.innerWidth, height: window.innerHeight }; const [backgroundImage] = useImage('https://konvajs.org/assets/line-building.png'); const getData = () => ({ '1st Floor': { color: 'blue', points: [366, 298, 500, 284, 499, 204, 352, 183, 72, 228, 74, 274], }, '2nd Floor': { color: 'red', points: [72, 228, 73, 193, 340, 96, 498, 154, 498, 191, 341, 171], }, '3rd Floor': { color: 'yellow', points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93], }, Gym: { color: 'green', points: [498, 283, 503, 146, 560, 136, 576, 144, 576, 278, 500, 283], }, }); const areas = getData(); const hoveredArea = ref(null); const tooltipProps = ref({ visible: false, x: 0, y: 0, text: '', }); const handleMouseOver = (key) => { hoveredArea.value = key; }; const handleMouseOut = () => { hoveredArea.value = null; tooltipProps.value.visible = false; }; const handleMouseMove = (e, key) => { const stage = e.target.getStage(); const mousePos = stage.getPointerPosition(); tooltipProps.value = { visible: true, x: mousePos.x, y: mousePos.y - 5, text: key, }; }; </script>