Hitz for Craft CMS

The front-end plan

One call

{{ craft.hitz.render('spring-fair') }}

That renders a container, the plan as JSON beside it, and registers Leaflet and the viewer. The visitor gets:

  • the whole plan fitted to the container, with the underlay if there is one;
  • halls underneath, stands and features on top, larger shapes below smaller ones;
  • stand codes as labels once they have zoomed in far enough to read them;
  • a tooltip on hover and a popup on click;
  • a legend, and a search box that finds stands by code or title;
  • a show whole plan button, and wheel-zoom that only engages once they click the map.

plan.render({ … }) does the same from a plan element.

Options

{{ craft.hitz.render('spring-fair', {
  height: '70vh',
  labels: 'code',
  popupTemplate: '_hitz/popup',
  linkField: 'exhibitor',
  highlight: entry.stand,
}) }}
OptionDefault
idrandomThe container's HTML id.
class—Extra classes on the container.
height—CSS height. Without it the container keeps the plan's aspect ratio.
labels'code''code', 'title', 'both' or false. Halls always show their title.
tooltipstrueCode and title on hover.
popupstrueA popup on click.
popupTemplate—A site template rendered per area with area and plan. Replaces the default popup.
linkField—Handle of a relation field on the area; its first element's URL becomes the popup link.
legendtrue
searchtrue
colorBy'availability' in Pro, else 'type''type' or 'availability'.
highlight—An area, an ID, a code, an element query, or a list of any of those. Outlined and zoomed to.
types—Only these area-type handles.
kinds—Only these kinds: hall, stand, feature.
deepLinktrueRead and write #hitz=A-12 in the address bar.
zoomControltrue
fitPadding16Pixels around the plan when fitted.
includeAssetstruefalse if you register the viewer's JS and CSS yourself.
backgroundtruefalse to leave the underlay off.

Disabled plans and disabled areas are never rendered.

Linking exhibitors and stands

Relations work in both directions, so model it whichever way your content already runs.

Stands on the exhibitor. Add a Hitz Areas field (stand) to your Exhibitor entry type:

{# exhibitor page: the plan, with this exhibitor's stand picked out #}
{{ craft.hitz.render('spring-fair', { highlight: entry.stand }) }}

Exhibitors on the stand. Add an Entries field (exhibitor) to the Stand area type's layout and use linkField:

{{ craft.hitz.render('spring-fair', { linkField: 'exhibitor' }) }}

Either way, relatedTo finds the other end:

{% set exhibitors = craft.entries.section('exhibitors').relatedTo(stand).all() %}

Custom popups

{# templates/_hitz/popup.twig — gets `area` and `plan` #}
{% set exhibitor = area.exhibitor.one() ?? null %}
<strong>{{ area.code }}</strong> {{ exhibitor.title ?? area.title }}
<p>{{ area.surfaceLabel }} · {{ area.hall.title ?? 'Foyer' }}</p>
{% if exhibitor %}<a href="{{ exhibitor.url }}">Visit exhibitor</a>{% endif %}

Popup templates are rendered on the server, once per area, when the page renders — keep them light on a plan with hundreds of stands, and eager-load inside them if they read relations.

Deep links

/floor-plan#hitz=B-07 opens the plan zoomed to stand B-07 with its popup open. Selecting a stand writes its code to the address bar, so a visitor can share it.

JavaScript

Each container gets a viewer on element.hitz:

const plan = document.querySelector('[data-hitz-plan]').hitz;
plan.focus('A-12');       // zoom to a stand and open its popup
plan.select(123);         // select by area ID
plan.map;                 // the Leaflet map
plan.areas;               // Map of area ID → { area, layer, label }

Events bubble from the container:

document.addEventListener('hitz:ready', (e) => { /* e.detail.viewer */ });
document.addEventListener('hitz:select', (e) => {
  console.log(e.detail.area.code, e.detail.area.title, e.detail.area.data);
});

Plans rendered later (Sprig, htmx, a modal) are booted with window.Hitz.boot(container).

Styling

Everything is a custom property on .hitz:

.hitz {
  --hitz-accent: #0f766e;
  --hitz-radius: 0;
  --hitz-bg: #fafaf9;
  --hitz-font: 'Inter', sans-serif;
}

Stand shapes carry hitz-area--stand and hitz-area--booked (etc.) classes for anything the properties do not reach.

Your own renderer

craft.hitz.planData('spring-fair') returns exactly what the viewer reads — the frame, the types, the legend and every area with its geometry, colour, code, surface and popup HTML — for a site that would rather draw it with SVG, D3 or MapLibre.