← Diagrams & maps

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

  1. Paste the snippet just before </head>.
  2. Add data-floor-plan-highlight to a container holding your floor plan as an inline SVG.
  3. Mark each room’s shape (a <rect>, <polygon> or <path>) with data-room and its name, and add data-info for a second line such as its size.

Options

Add any of these attributes to the same element.

data-roomOn a shape: the room’s name.
data-infoOn a shape: a tag shown under the name while it’s lit, such as 12 m².
data-label-x / data-label-yOn a shape: where its name goes, 0–100 across and down the plan. Default: the middle of the room.
data-colorHighlight colour, on the container or on a single room. Default #7c3aed.
data-labelsSet to off to show a room’s name only while it’s lit.
data-autoplayLight the rooms in turn while the plan is in view, pausing whenever someone points or taps.
data-intervalHow 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.

1 · Snippet
<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>
2 · Your HTML
<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>