Floor Plan Highlight
Point at a room on a floor plan and it lights up: its floor tints, its outline glows, and its name grows with a little tag showing its size, while the other rooms step back. On a phone, tap a room. Left alone, it walks through the rooms one by one.
How to use it
- Paste the snippet just before
</head>. - Add
data-floor-plan-highlightto a container holding your floor plan as an inline SVG. - Mark each room’s shape (a
<rect>,<polygon>or<path>) withdata-roomand its name, and adddata-infofor a second line such as its size.
Options
Add any of these attributes to the same element.
data-room | On a shape: the room’s name. |
data-info | On a shape: a tag shown under the name while it’s lit, such as 12 m². |
data-label-x / data-label-y | On a shape: where its name goes, 0–100 across and down the plan. Default: the middle of the room. |
data-color | Highlight colour, on the container or on a single room. Default #7c3aed. |
data-labels | Set to off to show a room’s name only while it’s lit. |
data-autoplay | Light the rooms in turn while the plan is in view, pausing whenever someone points or taps. |
data-interval | How long each room stays lit in autoplay. Default 1500 ms. |
Rooms can be reached with the Tab key too. Draw furniture and door swings with pointer-events="none" so they don’t get in the way of the rooms underneath.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Floor Plan Highlight: rooms on a floor plan light up as you hover over
// them (or tap them), with their name and size showing in the middle.
// Usage:
// <div data-floor-plan-highlight>
// <svg viewBox="0 0 400 260">
// <rect data-room="Kitchen" data-info="12 m²" x="200" y="20" width="100" height="90"/>
// …
// </svg>
// </div>
// Each room is an SVG shape with data-room (its name) and, if you like,
// data-info (a second line such as its size). Labels go in the middle of
// the room; move one with data-label-x / data-label-y (0-100 across and
// down the plan).
// Optional: data-color (highlight colour, default #7c3aed; also on a
// single room), data-labels="off" (only show a room's name while it is lit),
// data-autoplay (light the rooms in turn while in view, pausing when
// someone points or taps), data-interval (ms per room, default 1500).
var DEFAULTS = { color: '#7c3aed', interval: 1500, idle: 6000 };
var css =
'.fpl{position:relative;display:block;line-height:0}' +
'.fpl>svg{display:block;width:100%;height:auto;overflow:visible}' +
'.fpl [data-room]{transition:fill .35s,fill-opacity .35s,opacity .35s;cursor:pointer;outline:none}' +
'.fpl [data-room].fpl-on{fill:var(--fpl-c)!important;fill-opacity:.24}' +
'.fpl-ring{fill:none;stroke:var(--fpl-c);pointer-events:none;opacity:0;transition:opacity .35s;filter:drop-shadow(0 0 .25em var(--fpl-c))}' +
'.fpl-ring.fpl-on{opacity:1}' +
'.fpl-lab{position:absolute;translate:-50% -50%;line-height:1.2;text-align:center;pointer-events:none;white-space:nowrap;transition:transform .35s cubic-bezier(.3,1.5,.5,1),opacity .35s}' +
'.fpl-name{display:block;font-weight:700;font-size:.85em;color:#4b4060;transition:color .3s}' +
'.fpl-info{display:block;font-size:.75em;font-weight:600;color:#fff;background:var(--fpl-c);border-radius:99px;padding:.1em .6em;margin-top:.25em;opacity:0;transform:translateY(-.3em) scale(.8);transition:opacity .3s,transform .35s cubic-bezier(.3,1.5,.5,1)}' +
'.fpl-lab.fpl-on{transform:scale(1.18)}' +
'.fpl-lab.fpl-on .fpl-name{color:var(--fpl-c)}' +
'.fpl-lab.fpl-on .fpl-info{opacity:1;transform:none}' +
'.fpl-quiet .fpl-lab:not(.fpl-on){opacity:0}' +
'.fpl-dim .fpl-lab:not(.fpl-on){opacity:.45}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset, svg = el.querySelector('svg');
if (!svg) return;
var s = (el._fpl = { rooms: [], on: -1, idleUntil: 0 });
el.classList.add('fpl');
if (d.labels === 'off') el.classList.add('fpl-quiet');
el.style.setProperty('--fpl-c', d.color || DEFAULTS.color);
var vb = svg.viewBox.baseVal;
var live = !el.closest('[aria-hidden="true"]');
var ringLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
svg.appendChild(ringLayer);
svg.querySelectorAll('[data-room]').forEach(function (shape, i) {
var b = shape.getBBox();
var lx = shape.getAttribute('data-label-x'), ly = shape.getAttribute('data-label-y');
var lab = document.createElement('span');
lab.className = 'fpl-lab';
lab.setAttribute('aria-hidden', 'true');
lab.style.left = (lx !== null ? +lx : ((b.x + b.width / 2 - vb.x) / vb.width) * 100) + '%';
lab.style.top = (ly !== null ? +ly : ((b.y + b.height / 2 - vb.y) / vb.height) * 100) + '%';
var name = shape.getAttribute('data-room'), info = shape.getAttribute('data-info');
lab.innerHTML = '<span class="fpl-name"></span>' + (info ? '<span class="fpl-info"></span>' : '');
lab.firstChild.textContent = name;
if (info) lab.lastChild.textContent = info;
var c = shape.getAttribute('data-color');
if (c) { lab.style.setProperty('--fpl-c', c); shape.style.setProperty('--fpl-c', c); }
el.appendChild(lab);
// An outline drawn on top, so the lit room's edge glows over its walls.
var ring = shape.cloneNode(false);
['data-room', 'data-info', 'data-color', 'data-label-x', 'data-label-y', 'id', 'class', 'style', 'fill'].forEach(function (a) { ring.removeAttribute(a); });
ring.setAttribute('class', 'fpl-ring');
ring.setAttribute('stroke-width', Math.max(2, vb.width / 140));
if (c) ring.style.setProperty('--fpl-c', c);
ringLayer.appendChild(ring);
if (live) {
shape.setAttribute('tabindex', '0');
shape.setAttribute('role', 'img');
shape.setAttribute('aria-label', name + (info ? ', ' + info : ''));
shape.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') pick(el, i, true); });
shape.addEventListener('pointerdown', function () { pick(el, i, true); });
shape.addEventListener('focus', function () { pick(el, i, true); });
shape.addEventListener('blur', function () { if (s.on === i) pick(el, -1, true); });
}
s.rooms.push({ shape: shape, lab: lab, ring: ring });
});
if (live) {
svg.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') pick(el, -1, true); });
}
}
function pick(el, i, byUser) {
var s = el._fpl;
if (byUser) s.idleUntil = Date.now() + DEFAULTS.idle;
s.on = i;
s.rooms.forEach(function (r, k) {
r.shape.classList.toggle('fpl-on', k === i);
r.ring.classList.toggle('fpl-on', k === i);
r.lab.classList.toggle('fpl-on', k === i);
});
el.classList.toggle('fpl-dim', i >= 0);
}
function tick(el) {
var s = el._fpl, every = +el.dataset.interval || DEFAULTS.interval;
clearTimeout(s.timer);
if (!s.visible) return;
if (Date.now() >= s.idleUntil) {
// Light a room, then a short dark pause every lap.
s.step = ((s.step === undefined ? -1 : s.step) + 1) % (s.rooms.length + 1);
pick(el, s.step < s.rooms.length ? s.step : -1, false);
}
s.timer = setTimeout(function () { tick(el); }, s.step === s.rooms.length ? every / 2 : every);
}
function intro(el) {
var labs = el._fpl.rooms.map(function (r) { return r.lab; });
anime.animate(labs, { opacity: [0, 1], y: [8, 0], duration: 500, delay: anime.stagger(90), ease: 'out(3)',
onComplete: function () { labs.forEach(function (l) { l.style.opacity = ''; l.style.transform = ''; }); } });
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target, s = el._fpl;
s.visible = e.isIntersecting;
if (!e.isIntersecting) { clearTimeout(s.timer); return; }
if (!s.played) { s.played = true; if (!reduced()) intro(el); }
if (el.dataset.autoplay !== undefined && !reduced()) {
clearTimeout(s.timer);
s.timer = setTimeout(function () { tick(el); }, 900);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-floor-plan-highlight]').forEach(function (el) {
if (el._fpl) return;
setup(el);
if (el._fpl) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-floor-plan-highlight data-autoplay>
<svg viewBox="0 0 400 260">
<rect data-room="Living room" data-info="24 m²" x="20" y="20" width="180" height="130" fill="#fff" stroke="#333" stroke-width="3"/>
<rect data-room="Kitchen" data-info="11 m²" x="200" y="20" width="100" height="90" fill="#fff" stroke="#333" stroke-width="3"/>
</svg>
</div>