Guides

How to build an interactive seat map for your website

Updated

A clickable seat map does not need a heavy library. An SVG in which every seat is its own element with its data attached, one click listener and a lookup into your inventory are enough.

Who this guide is for

This guide is for developers and ticketing teams who already have a booking system, or are building one, and want a seat map buyers can click. It covers the front end: getting the map onto a page, reacting to clicks, showing status and keeping the map in step with your inventory. What data a seat map needs in the first place is covered in what a seat map needs for online booking. The examples use the attribute names that seatorio's SVG export writes, but the approach works for any SVG built the same way.

Inline the SVG, do not link it

An SVG shown with an img tag or as a CSS background is a picture: the browser draws it, but your scripts cannot reach the seats inside it. To make seats clickable, the SVG markup has to be part of the page's DOM. There are two simple ways:

  • Server side: include the SVG file's contents in the page template when you render it.
  • Client side: fetch the SVG as text and insert it into a container element.

Either way, keep the viewBox attribute and let CSS control the size, for example a width of 100% and an automatic height, so the map scales to the screen. On phones, a seat map with thousands of seats needs zoom and pan. You can add that with a small pan and zoom script, or by changing the viewBox yourself when the buyer picks a section.

What is on each seat

In an SVG exported from seatorio, every bookable place is one element, and that element carries these data attributes:

AttributeWhat it holds
data-seat-idThe seat's id in the plan. It is the same id as in the JSON export.
data-seat-uuidA hash of the section name, row label and seat number (for a chair: the table label and the chair number). The same labels always give the same uuid, and the JSON export carries the same value as seat_uuid.
data-section-idThe id of the section the seat belongs to.
data-seat-statusThe status at export time: free unless something else was set, and oos (out of service) for a seat marked not for sale.
data-table-idOnly on chairs at a table: the table's id.

Section outlines carry data-section-id, data-section-name and data-reservation-mode, plus data-section-category when the section has a category. A section sold by capacity rather than by seat, such as a standing area, has data-reservation-mode="per-capacity" and data-capacity with the number of places: the outline itself is what the buyer clicks.

Row labels and seat numbers are drawn as text on the map but are not attributes on the seat element. They are in the JSON export, which you join to the SVG by seat id, as shown below.

One listener for the whole map

Do not attach a click handler to every seat. A stadium has tens of thousands of them. Attach one listener to the SVG and find the seat the click landed on. This is called event delegation:

<div id="seat-map">
  <!-- the exported SVG, included by the server or inserted by script -->
</div>
const svg = document.querySelector('#seat-map svg');

svg.addEventListener('click', function (event) {
  const seat = event.target.closest('[data-seat-id]');
  if (!seat) return;
  if (seat.dataset.seatStatus !== 'free') return;

  const info = seatsById[seat.dataset.seatId];
  selectSeat({
    id: seat.dataset.seatId,
    uuid: seat.dataset.seatUuid,
    section: seat.dataset.sectionId,
    label: info ? info.label : ''
  });
});

The browser exposes data-seat-id as dataset.seatId, data-seat-status as dataset.seatStatus and so on. closest() handles clicks on anything inside a seat. The function selectSeat is yours: it might add the seat to a basket, ask your server to hold it or open a price panel.

Join the SVG to the JSON export

The JSON export holds the full plan: sections with their names and categories, and inside each section its seats with id, rowLabel, seatNumber, status and seat_uuid. Build a lookup once, keyed by seat id:

const seatsById = {};
plan.sections.forEach(function (section) {
  (section.seats || []).forEach(function (seat) {
    seatsById[seat.id] = {
      section: section.name,
      category: section.category,
      row: seat.rowLabel,
      number: seat.seatNumber,
      label: (seat.rowLabel || '') + ' ' + seat.seatNumber
    };
  });
});

Chairs at tables carry a tableId instead of a row; the table's label is in the section's tables list. Either key joins the two files of one export: the id and the uuid are the same values in the SVG and in the JSON.

Colour seats by status

Status lives in data-seat-status, so changing it is one attribute and CSS does the rest. The exported SVG embeds a small stylesheet: seats get a pointer cursor, reserved seats are dimmed, sold seats are grey, seats not for sale (oos) keep their grey and lose the pointer, and a section outline marked data-section-status="sold" stops looking clickable. Add your own rules for any other statuses you use, such as blocked or held.

function setStatus(seatId, status) {
  const seat = svg.querySelector('[data-seat-id="' + seatId + '"]');
  if (seat) seat.setAttribute('data-seat-status', status);
}

Your booking system is the source of truth for availability. When the page loads, fetch the current statuses for the event or date from your own API and apply them, then refresh them while the buyer is choosing, by polling or a push channel. The status written into the file at export time is only a starting point. In seatorio, statuses are set in the editor for beach sets (free, reserved, occupied, out of service) and parking bays (free, occupied, out of service); other seats export as free, or oos when they are marked not for sale, and your system changes them. For beach sets and parking bays the clickable element is an outline over the drawn set or bay, so check your status styles on those plans too.

Map seats to your inventory

Your inventory needs a key for each seat that does not drift. The export gives you two, and they behave differently:

  • The seat id stays the same while the seat exists: moving it, rotating it, renumbering its row or renaming its section does not change it. A seat that is deleted and drawn again gets a new id.
  • The seat uuid is calculated from the section name, row label and seat number. It survives deleting and redrawing a seat, or rebuilding the whole plan, as long as the labels stay the same. Renaming the section or renumbering the row changes it.

Pick the one that matches how your venue changes. A venue whose labels are fixed by printed tickets and signs usually keys on the uuid. A venue that renames areas often keys on the id. Many teams store both. Either way, check that labels are unique within each section before you rely on the uuid, because two seats with the same section, row and number produce the same uuid.

Keep ids stable across edits

Venues change between seasons, so plan for re-exports from the start:

  1. Keep one master plan and edit it, instead of drawing a new plan each season.
  2. After each export, compare the new list of ids or uuids with the previous one.
  3. Treat added seats as new inventory, removed seats as retired, and do not reuse their keys.
  4. Check that no seat with an existing order has disappeared before you publish the new map.

Accessibility and small screens

A map alone is hard to use with a keyboard or a screen reader. Offer a list or a best-available option next to it, give seats a readable label from the JSON (section, row, seat) in your own markup, and do not rely on colour alone: dimmed and grey seats should also refuse clicks and say why. On phones, show one section at a time, and make sure seats are large enough to tap at the zoom level you open with.

Checklist

  1. The SVG is inlined in the page, not shown as an image.
  2. One click listener on the SVG, using closest('[data-seat-id]').
  3. Labels and categories come from the JSON, joined by seat id.
  4. Statuses come from your booking system on load and are refreshed.
  5. CSS covers every status you use.
  6. Your inventory keys on the id, the uuid or both, by choice.
  7. Each re-export is compared with the last before it goes live.
  8. Keyboard, screen reader and phone users have a way to choose.

How seatorio helps

seatorio draws and numbers the plan and exports it in the two forms this guide uses. The SVG has every seat as its own element with data-seat-id, data-seat-uuid, data-section-id and data-seat-status (and data-table-id for chairs at tables), section outlines with their name, category and reservation mode, and a small embedded stylesheet for pointer, reserved, sold and not-for-sale styles. The JSON holds the whole plan with row and seat labels, section categories and a uuid per seat, and it can be imported back into the editor.

seatorio does not host the map on your site, sell tickets or update availability: that part is your booking system. A live seat map for websites is listed as coming soon on the pricing page, and exports on the Free plan carry a small watermark. To see an export with thousands of seats, start from the stadium seating chart template, or read about seatorio for stadiums.

Planning for stadiums & arenas? See seatorio for stadiums & arenas and the stadium seating chart template.

More guides