Custom Seat States (Renderer SDK)

Custom seat states let you visually mark individual seats with an arbitrary, application-defined state – for example reserved, held, vip, or sold – on top of whatever the renderer is already drawing. A state is a string key: you assign that key to a set of seats at runtime with setSeatsState, and you describe how the key should look in the renderer’s theme.seatStyles.

This guide covers the client-side renderer SDK (@seatmap.pro/renderer). It explains how to apply and visualize states from the browser. If you are looking for the backend seat lifecycle and customer-defined hold types (the state / holdType data model on the booking service), see Seat States & Hold Types instead – the two are complementary.

Overview

The feature is built from two independent layers. Keeping them separate in your mind is the key to using the API correctly.

Layer What it controls How you drive it
Assignment engine Which seats are in which state setSeatsState(seats, key) / clearSeatsState(seats)
Style config What a state key looks like theme.seatStyles (declarative, set at construction)

The assignment engine maintains a mapping from seat ID to a single state key (at most one key per seat). It knows nothing about colors, sizes, or the DOM. The style config maps each state key to a style description and lives in the renderer settings you pass at construction time; it is never mutated at runtime by the state methods.

A seat produces a visible overlay only when both layers agree: the engine has assigned it a key, and seatStyles has an entry for that key.

The overlay is a DOM layer

What actually appears on screen is a DOM overlay – a single absolutely-positioned <div> container appended into the renderer’s host element, with one small <div> node per visible, assigned seat. It is not painted into the WebGL or Canvas2D stage. Because it reads only the shared viewport transform (scale / translate), it works identically under both the WebGL renderer and the Canvas2D fallback. The container uses pointer-events: none, so it never intercepts canvas input.

Quick Start

The smallest end-to-end flow: construct a renderer with one custom state in theme.seatStyles, load an event, then assign that state to some seats after the seats exist.

import { SeatmapBookingRenderer } from '@seatmap.pro/renderer';

const element = document.getElementById('seatmap') as HTMLElement;

const renderer = new SeatmapBookingRenderer(element, {
  publicKey: 'your-public-key',
  theme: {
    seatStyles: {
      mykey: {
        tint: 'rgba(76,141,255,0.5)',
        keepSeatName: true,
      },
    },
  },
});

void renderer.loadEvent('event-guid').then(() => {
  renderer.setSeatsState([101, 102, 103], 'mykey');
});

Notes:

  • publicKey is required at runtime for SeatmapBookingRenderer; the constructor throws 'Public key is undefined' if it is missing.
  • setSeatsState must be called after seats exist – that is, after loadEvent resolves (or from inside an onSchemaDataLoaded callback). States are applied to seats that have already loaded.
  • Overlay nodes are tiny at fit-to-venue zoom. To make them clearly visible, zoom into a section with zoomToSection (see the worked examples).

Concepts

Assignment engine semantics

  • One state per seat. Each seat has exactly one assigned key, or none.
  • Overwrite, not merge. Assigning a new state to a seat drops any prior assignment. Assigning reserved then sold to the same seat leaves it as sold.
  • Clear is per-seat and silent on misses. Clearing the state on IDs that were not assigned simply skips them; a partial clear leaves other seats untouched.
  • Blocking is a separate gate. A seat is treated as blocked only when it has an assignment and the style for that key sets blockInteraction truthy. State alone never implies blocking.

Built-in keys vs custom keys

State keys fall into two groups.

Built-in keys are the renderer’s own seat states, enumerated by BuiltinSeatStateKey:

type BuiltinSeatStateKey =
  | 'default'
  | 'unavailable'
  | 'filtered'
  | 'hovered'
  | 'selected'
  | 'loading'
  | 'error';

These are configured with the richer ISeatStyle type (size, color, stroke, shadow, accessible override). The shipped booking and admin themes already populate them. In particular, the default entry’s size is the footprint every overlay node uses (see Performance and Rendering Notes).

Custom keys are any other string – 'reserved', 'held', 'vip', 'sold', 'restricted', and so on. You assign them via setSeatsState and style them with the lighter ICustomSeatStyle (or a full ISeatStyle if you prefer).

The Style Config

All seat styling lives in theme.seatStyles, typed as SeatStylesMap.

SeatStylesMap shape

type SeatStylesMap = Partial<Record<BuiltinSeatStateKey, ISeatStyle>> & {
  [customKey: string]: ICustomSeatStyle | ISeatStyle | undefined;
};
  • The Partial<Record<BuiltinSeatStateKey, ISeatStyle>> half constrains the seven built-in keys to ISeatStyle (the richer type, which adds the accessible override). All seven are optional.
  • The index-signature half opens the map to any arbitrary string key, whose value may be either an ICustomSeatStyle (lightweight: tint, svg, render callback, etc.) or a full ISeatStyle. The undefined in the union is required by TypeScript when mixing a Partial<Record<...>> with a general index signature.

In practice: built-in keys get ISeatStyle; custom keys typically use the lighter ICustomSeatStyle.

IBasicSeatStyle

interface IBasicSeatStyle {
  size: number;
  color: string;
  seatName?: {
    font: string;
    color: string;
  };
  stroke?: {
    width: number;
    color: string;
    align: 'center' | 'inside' | 'outside';
  };
  imageId?: string;
  shadow?: {
    blur: number;
    color: string;
    x?: number;
    y?: number;
  };
}
Field Type Optional Meaning
size number No Seat render size (diameter/radius in the renderer’s unit).
color string No Seat fill color.
seatName { font: string; color: string } Yes Seat label typography. font is a CSS font string (e.g. '18px Arial'); color is the text color.
stroke { width: number; color: string; align: 'center' | 'inside' | 'outside' } Yes Seat border: thickness, color, and alignment relative to the seat edge.
imageId string Yes ID of a pre-registered image asset drawn on the seat.
shadow { blur: number; color: string; x?: number; y?: number } Yes Drop-shadow: blur radius, color, and optional X/Y offset.

ISeatStyle

interface ISeatStyle extends IBasicSeatStyle {
  accessible?: IBasicSeatStyle;
}
Field Type Optional Meaning
(inherits all IBasicSeatStyle fields) See table above.
accessible IBasicSeatStyle Yes Override style applied when the seat is flagged accessible/wheelchair. Carries the same six base fields, minus accessible itself.

ICustomSeatStyle

interface ICustomSeatStyle {
  tint?: string;
  svg?: string;
  imageId?: string;
  className?: string;
  priority?: number;
  blockInteraction?: boolean;
  keepSeatName?: boolean;
  render?: (args: ISeatStateRenderArgs) => HTMLElement | string;
}
Field Type Optional Meaning
tint string Yes Color tint overlaid on the seat footprint (rendered as a filled circle).
svg string Yes SVG to draw on the seat. If it starts with <, it is treated as inline SVG markup; otherwise it is used as a URL.
imageId string Yes ID of a pre-registered image asset (looked up in theme.images[imageId]).
className string Yes CSS class name added to the seat’s overlay <div> (for custom styling / animation).
priority number Yes Accepted by the type but not currently consumed by the overlay. There is no z-ordering, and setting it has no effect today.
blockInteraction boolean Yes When true, the seat is reported as blocked (see Interaction Blocking).
keepSeatName boolean Yes Controls the seat label on the overlay node. The label is shown by default; only keepSeatName: false suppresses it.
render (args: ISeatStateRenderArgs) => HTMLElement | string Yes Custom DOM callback that fully supplies the seat’s overlay content.

Note: ICustomSeatStyle has no size of its own. Overlay nodes share the default seat footprint – the size comes from seatStyles['default']?.size (falling back to 10 if there is no default entry).

ISeatStateRenderArgs

The single argument passed to a custom render callback:

interface ISeatStateRenderArgs {
  seat: ISeat;
  stateKey: string;
  style: ICustomSeatStyle;
  sizePx: number;
}
Field Type Meaning
seat ISeat The full seat data object being rendered.
stateKey string The active state key (built-in or custom) that triggered this render.
style ICustomSeatStyle The resolved ICustomSeatStyle for this state, so the callback can inspect its own config.
sizePx number The seat’s current rendered size in pixels, so the callback can scale its output.

The Public API

Both methods are public on the base renderer, so SeatmapBookingRenderer and SeatmapAdminRenderer expose them identically. Apply states only after the event and its seats have loaded.

setSeatsState

setSeatsState(seats: ISeat[] | number[] | string[], stateKey: string): void

Assigns stateKey to the given seats: resolves the seat IDs, records the assignment in the engine (overwriting any prior state), writes customState onto each matching seat object, then redraws so the overlay re-syncs. If seats is empty, the call does nothing.

clearSeatsState

clearSeatsState(seats: ISeat[] | number[] | string[]): void

Clears the state from the given seats: resolves the IDs, clears the engine assignment (misses are silently skipped), removes customState from each matching seat object, then redraws. If seats is empty, the call does nothing.

Accepted argument forms

Both methods accept the same union for seats:

  • ISeat[] – seat objects (resolved by their .id).
  • number[] – raw numeric seat IDs.
  • string[] – composite seat keys (resolved to IDs internally).

The dispatch type-sniffs the first element to decide how to resolve IDs.

Clearing every seat

There is no zero-argument “clear all” overload on the renderer. To clear every seat, pass all seat IDs explicitly via getSeats:

renderer.clearSeatsState(renderer.getSeats().map((s) => s.id));

Defining Custom States

A custom state is a seatStyles entry under an arbitrary key. Mix and match the ICustomSeatStyle fields:

  • Tint / color. tint overlays a filled circle in a CSS color over the seat footprint. (For built-in keys styled with ISeatStyle, color is the seat fill.)
  • Size. A custom state has no independent size; it shares the default seat footprint (seatStyles['default']?.size, fallback 10). ISeatStateRenderArgs.sizePx reports the resolved value.
  • Stroke. Available on IBasicSeatStyle / ISeatStyle (width, color, align). Use a full ISeatStyle entry if you need it.
  • Shadow. Available on IBasicSeatStyle / ISeatStyle (blur, color, x, y).
  • Seat name / keepSeatName. The overlay node shows the seat label by default; set keepSeatName: false to suppress it. seatName typography (font, color) is part of the built-in ISeatStyle.
  • className. Adds a CSS class to the overlay <div> so you can style or animate it from your stylesheet.
  • imageId. Draws a pre-registered image asset, resolved from theme.images[imageId].
  • priority. Declared on ICustomSeatStyle but not currently read by the overlay. No z-ordering is implemented, and because each seat holds at most one state there is never more than one custom state on a seat to order. Setting it has no effect today.
  • blockInteraction. Reported through the blocking gate (see Interaction Blocking).

Example custom states:

const seatStyles: SeatStylesMap = {
  restricted: {
    tint: 'rgba(214,88,78,0.6)',
    blockInteraction: true,
    keepSeatName: false,
  },
  promo: {
    tint: 'rgba(76,141,255,0.5)',
    className: 'promo-seat',
  },
};

Overriding Built-in Seat States

The same theme.seatStyles map that declares custom keys also overrides the renderer’s own built-in states. The difference from custom keys is where they are drawn and what drives them: built-in keys restyle the seat glyph painted on the canvas stage, and the renderer applies them automatically from each seat’s own condition (locked, filtered, hovered, selected, and so on). You do not call setSeatsState for them.

Built-in key Applied when the seat is…
default available, not hovered or selected
unavailable locked / not for sale
filtered filtered out of the current view
hovered under the pointer (and interactive)
selected selected / in the cart
loading in the loading state
error in the error state

Your seatStyles is deep-merged over the shipped default theme at construction. You can therefore override a single field of a built-in state and inherit the rest – there is no need to restate the whole style. For example, to make selected seats larger and green with a thicker border, and to recolor unavailable seats:

const renderer = new SeatmapBookingRenderer(element, {
  publicKey: 'your-public-key',
  theme: {
    seatStyles: {
      selected: {
        size: 46,
        color: '#1f9d55',
        stroke: { width: 3, color: '#0b5d2e', align: 'outside' },
      },
      unavailable: { size: 8, color: '#d0d0d0' },
    },
  },
});

Notes:

  • Built-in keys take the richer ISeatStyle (size, color, seatName, stroke, imageId, shadow, accessible). Supply only the fields you want to change; the rest come from the default theme via the deep merge.
  • This styles the canvas seat glyph, independent of the DOM custom-state overlay. The two compose: a seat can be drawn selected on the canvas and still carry a custom-state overlay on top.
  • default.size also defines the footprint of every custom-state overlay node (see Performance and Rendering Notes), so changing it resizes both the base seats and the overlays.

Recipe: Price Colors, State Images, and Seat Numbers Together

A common need is to color seats by price tier (so buyers see the price) and overlay a transactional state – reserved, held, sold – as an icon or SVG mask on top, while keeping the seat number readable. Drawn as a single canvas glyph, that fights itself:

  1. An image drawn directly on the seat (via imageId on the seat style, or an onBeforeSeatDraw override) replaces the whole glyph, so the seat number disappears – the seat-draw path returns as soon as the image is drawn and never paints the label.
  2. Because that image is the seat glyph, it is drawn at the image’s own pixel size; sizing it to fill the seat circle is awkward.
  3. Setting the image per-seat through onBeforeSeatDraw returns a fresh ISeatStyle, which replaces the whole style and drops the price/category color you set with setSeatsCategory.

The custom-state overlay avoids all three, because it is a separate DOM layer drawn on top of the canvas, not a replacement for the seat glyph:

  • The price color stays put. setSeatsState never touches seat.special.category or the category-color map, so a seat keeps the color you assigned with setSeatsCategory.
  • The image fills the seat. The overlay draws the state’s svg / imageId at the full seat footprint (inset: 0, width and height equal to the default seat size).
  • The number stays on top. The seat number is re-drawn centered over the image by default (keepSeatName defaults to true); set keepSeatName: false only when you want the icon to replace the number (the lock on sold, for instance).

So replace the per-seat onBeforeSeatDraw approach with two independent, composable calls:

// 1) Price-tier color, painted on the canvas seat -- persists.
renderer.setSeatsCategory(seatIds, priceCategoryId, '#1f9d55');

// 2) Transactional state overlay on top -- seat number preserved.
renderer.setSeatsState(seatIds, 'reserved');

with the state declared once at construction. Omit tint so the price color shows through the transparent parts of the mask:

const seatStyles: SeatStylesMap = {
  reserved: {
    keepSeatName: true, // default -- seat number stays visible on top
    svg:
      '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
      '<circle cx="50" cy="50" r="42" fill="none" stroke="#fff" stroke-width="6"/></svg>',
  },
};

The seat keeps its price color, the SVG fills the seat as an overlay mask, and the number renders on top – with no onBeforeSeatDraw. To recolor the seat for the state instead of letting the price color show through, add a semi-transparent tint. To reference a named asset instead of inline SVG, register it under theme.images and point imageId at it:

const renderer = new SeatmapBookingRenderer(element, {
  publicKey: 'your-public-key',
  theme: {
    images: { holdIcon: 'data:image/svg+xml;utf8,...' }, // or a URL
    seatStyles: {
      held: { imageId: 'holdIcon', keepSeatName: true },
    },
  },
});

The overlay is tiny at fit-to-venue zoom and is hidden in the detailed single-section view; see Performance and Rendering Notes if a state does not appear. Because each state seat is a separate DOM node, keep the number of seats carrying a state that are visible at once bounded – the overlay is not batched like the canvas seats, so large numbers degrade performance.

Custom DOM via render()

When the declarative fields are not enough, supply a render callback. It fully provides the overlay node’s content for that state.

render?: (args: ISeatStateRenderArgs) => HTMLElement | string;

The callback receives one ISeatStateRenderArgs (seat, stateKey, style, sizePx) and returns either:

  • an HTMLElement – appended directly to the overlay node; use this for complex or interactive content, or
  • a string – treated as raw SVG markup. The renderer wraps it in an absolutely-positioned <div> (inset: 0) and renders it via an <img src="data:image/svg+xml;utf8,..."> sized to fill the node.

When render is present, it takes over content generation for the node; the declarative tint / svg / imageId path is not used for that state. Use render for dynamic, data-driven visuals (for instance, drawing something based on seat or sizePx). Prefer the declarative fields for static visuals – they are simpler and need no callback.

const seatStyles: SeatStylesMap = {
  badge: {
    render: ({ sizePx }: ISeatStateRenderArgs): string =>
      `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sizePx} ${sizePx}">` +
      `<circle cx="${sizePx / 2}" cy="${sizePx / 2}" r="${sizePx / 3}" fill="#fff"/></svg>`,
  },
};

Animations

There are two animation paths.

Self-contained SVG animations (SMIL)

The canonical custom states animate entirely inside the SVG string itself, using SMIL elements (<animate>, <animateTransform>). This needs no external CSS, no @keyframes, and no className – for example, an <animate> on a circle’s r and stroke-opacity produces a pulsing ring. This is the approach used by the worked examples below.

CSS animations via className

If you prefer CSS-driven animation, set className on the custom state. The overlay node receives that class on its <div>, and you animate it from your own stylesheet:

.promo-seat {
  animation: promo-pulse 1.4s ease-in-out infinite;
}

@keyframes promo-pulse {
  0%,
  100% {
    opacity: 0.4;
  }
  50% {
    opacity: 1;
  }
}
const seatStyles: SeatStylesMap = {
  promo: { tint: 'rgba(76,141,255,0.5)', className: 'promo-seat' },
};

Interaction Blocking

A seat is reported as blocked only when both conditions hold:

  1. The seat has an assigned state key.
  2. seatStyles[stateKey] exists and has blockInteraction truthy.

Therefore:

  • A seat with no assignment is never blocked.
  • A seat whose style lacks blockInteraction (or has it falsy) is not blocked.
  • If the seatStyles map is absent, nothing is blocked.

Concretely, with { sold: { blockInteraction: true }, held: { tint: '#4c8dff' } }: a seat in sold is blocked, a seat in held is not.

keepSeatName is independent of blocking. The label is shown by default; only keepSeatName: false suppresses it. In the worked examples below, sold sets keepSeatName: false (the lock icon replaces the number), while reserved, held, and vip set keepSeatName: true.

Booking vs Admin Renderer

setSeatsState and clearSeatsState are defined on the base renderer and inherited unchanged by both SeatmapBookingRenderer and SeatmapAdminRenderer. The API, argument forms, and behavior are identical on both.

The constructors differ only in their settings types.

SeatmapBookingRenderer:

constructor(
  element: HTMLElement,
  settings?: IBookingRendererSettings,
  tags?: Record<string, string | number | boolean>,
)

SeatmapAdminRenderer:

constructor(element: HTMLElement, settings?: IAdminRendererSettings)

Both settings types accept settings.theme.seatStyles.

Why a demo runs on the admin renderer

When you want to demonstrate and visually verify custom states, the admin renderer is the easier surface – for rendering visibility, not API capability. On the booking renderer, seat availability for a real event drives appearance: demo seats that are not actually available read as unavailable, and the unavailable styling can obscure the custom-state overlay. The admin renderer does not apply that booking-availability styling, so the states show cleanly. In production, the booking renderer exposes the exact same methods for genuinely available seats.

Worked Examples

The following reproduces four canonical states and the full flow. It is copy-pasteable.

The four states in theme.seatStyles

const seatStyles: SeatStylesMap = {
  reserved: {
    tint: 'rgba(224,161,60,0.55)',
    keepSeatName: true,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="6"><animate attributeName="r" values="34;46;34" dur="1.4s" repeatCount="indefinite"/><animate attributeName="stroke-opacity" values="0.9;0.3;0.9" dur="1.4s" repeatCount="indefinite"/></circle></svg>',
  },
  held: {
    tint: 'rgba(76,141,255,0.5)',
    keepSeatName: true,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="42" fill="none" stroke="#fff" stroke-width="5"><animate attributeName="stroke-opacity" values="0.15;0.85;0.15" dur="2.2s" repeatCount="indefinite"/></circle></svg>',
  },
  vip: {
    tint: 'rgba(155,108,255,0.5)',
    keepSeatName: true,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="none" stroke="#fff" stroke-width="5" stroke-dasharray="30 200" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="2.4s" repeatCount="indefinite"/></circle></svg>',
  },
  sold: {
    tint: 'rgba(214,88,78,0.6)',
    blockInteraction: true,
    keepSeatName: false,
    svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5m0 2a3 3 0 0 1 3 3v3H9V6a3 3 0 0 1 3-3"/></svg>',
  },
};

Per-state summary:

  • reservedtint, keepSeatName: true, animated pulse ring (SMIL <animate> on r and stroke-opacity, 1.4 s).
  • heldtint, keepSeatName: true, fading ring (SMIL <animate> on stroke-opacity, 2.2 s).
  • viptint, keepSeatName: true, rotating dashed ring (SMIL <animateTransform> rotate, 2.4 s).
  • soldtint, blockInteraction: true, keepSeatName: false, static lock icon (no animation).

None of the four states use color, size, stroke, render, or className.

Picking a demo section

Group seats by their section and return the first section with at least 44 seats, otherwise the largest:

const pickCustomStateDemoSection = (
  renderer: SeatmapAdminRenderer,
): { sectionId: number; ids: number[] } => {
  const bySection = new Map<number, number[]>();
  for (const seat of renderer.getSeats()) {
    const list = bySection.get(seat.sectorId) ?? [];
    list.push(seat.id);
    bySection.set(seat.sectorId, list);
  }
  let best: { sectionId: number; ids: number[] } | null = null;
  for (const [sectionId, ids] of bySection) {
    if (ids.length >= 44) return { sectionId, ids };
    if (!best || ids.length > best.ids.length) best = { sectionId, ids };
  }
  return best ?? { sectionId: 0, ids: [] };
};

The mixed demo flow

Clear every seat, apply the four states across disjoint slices of the demo section, then zoom in:

const demoMix = (renderer: SeatmapAdminRenderer): void => {
  const { sectionId, ids } = pickCustomStateDemoSection(renderer);
  const pick = (from: number, to: number): number[] => ids.slice(from, to);
  renderer.clearSeatsState(renderer.getSeats().map((s) => s.id));
  renderer.setSeatsState(pick(0, 12), 'reserved');
  renderer.setSeatsState(pick(12, 24), 'held');
  renderer.setSeatsState(pick(24, 32), 'vip');
  renderer.setSeatsState(pick(32, 44), 'sold');
  renderer.zoomToSection(sectionId, { focus: true });
};

The zoomToSection(sectionId, { focus: true }) call is what makes the small overlay nodes visible.

Single-state actions

Each single-state action applies one state to its slice (without first clearing), then zooms in:

const applyReserved = (renderer: SeatmapAdminRenderer): void => {
  const { sectionId, ids } = pickCustomStateDemoSection(renderer);
  renderer.setSeatsState(ids.slice(0, 12), 'reserved');
  renderer.zoomToSection(sectionId, { focus: true });
};

The held, vip, and sold actions follow the same shape, each on its own slice of ids: held on ids.slice(12, 24), vip on ids.slice(24, 32), and sold on ids.slice(32, 44).

Clearing all states

const clearStates = (renderer: SeatmapAdminRenderer): void => {
  const ids = renderer.getSeats().map((s) => s.id);
  renderer.clearSeatsState(ids);
};

Performance and Rendering Notes

The overlay keeps DOM cost proportional to what is actually on screen – but it is real DOM, so the number of state seats visible at the same time governs performance.

  • Cost scales with the number of visible state seats – keep it bounded. Every visible assigned seat mounts its own <div> node, plus an inner <img> for an svg / imageId, an optional tint layer, and the seat-number node. Unlike the WebGL seat layer, these are individual DOM elements, not batched – and animated (SMIL) SVGs each run their own animation. A handful to a few dozen state seats on screen is fine; applying states to hundreds or thousands of seats that are visible at once – a whole large section, or any state set at a zoom where much of the venue is in view – creates that many nodes and degrades frame rate significantly, the more so with animation. For styling that must scale to the whole venue, prefer the canvas-batched paths: price/category colors via setSeatsCategory, and built-in state overrides in seatStyles. Reserve the custom-state overlay for a bounded set of seats the user is actually looking at, and prefer static (non-animated) visuals when many seats carry a state at once.
  • Virtualization. A node is mounted only for seats in the intersection of the visible set and the assigned set. Seats outside the viewport are never mounted; seats that scroll out of view have their nodes removed.
  • Lazy container. No container <div> is created until at least one state is assigned. When no states remain, the overlay tears down completely – nodes cleared, container removed from the DOM. Clearing all states removes the overlay entirely.
  • Section-view suppression. When the renderer enters the detailed single-section view, the overlay is suppressed (display: none) and reconciliation is skipped. It reappears when suppression lifts. This is tied to section-detail view, not to 3D mode.
  • Tiny at fit-to-venue. Node size comes from seatStyles['default']?.size (fallback 10) and the nodes are positioned in world coordinates, scaled by the shared viewport transform. At fit-to-venue zoom the nodes are very small. This is why the examples call zoomToSection(sectionId, { focus: true }).
  • Stage independence. The overlay reads only stage-agnostic state – the viewport transform, the host element, devicePixelRatio, and theme.images for imageId resolution. It behaves identically under WebGL and the Canvas2D fallback.

Troubleshooting

Overlay not visible.

  • Zoom level. At fit-to-venue, nodes are tiny. Call zoomToSection(sectionId, { focus: true }) (or otherwise zoom into the section) to make them visible.
  • Section-view suppression. In the detailed single-section view the overlay container is hidden. It returns when suppression lifts. 3D rotation does not suppress the overlay.
  • Seats not yet loaded. State applies only to loaded seats. Call setSeatsState after loadEvent resolves (or from onSchemaDataLoaded).
  • Wrong key / not in seatStyles. A seat only renders an overlay when its assigned key has a matching seatStyles entry. Verify the key string passed to setSeatsState exactly matches a key in theme.seatStyles.

States not clearing.

  • clearSeatsState clears only the IDs you pass; misses are silently skipped. To clear everything, pass all IDs: renderer.clearSeatsState(renderer.getSeats().map((s) => s.id)). When the last state is cleared, the entire overlay container is removed from the DOM.

Booking-renderer demo seats appear unavailable.

  • On the booking renderer, real event availability styles unavailable seats, which can obscure the custom-state overlay. Demonstrate custom states on SeatmapAdminRenderer, which does not apply booking-availability styling. In production on the booking renderer, apply custom states to genuinely available seats.

API Quick Reference

Symbol Signature / Shape Where
setSeatsState (seats: ISeat[] | number[] | string[], stateKey: string): void Renderer (booking + admin)
clearSeatsState (seats: ISeat[] | number[] | string[]): void Renderer (booking + admin)
setSeatsCategory (seats: ISeat[] | number[] | string[], category: number, color?: string): void Renderer (booking + admin)
getSeats () => ISeatDTO[] (used to enumerate seats for clear-all) Renderer
zoomToSection (sectionId, { focus: true }) Renderer
loadEvent booking: (eventId: string, sectorId?: number) => Promise<void>; admin: (eventId: string) => Promise<void> renderers
BuiltinSeatStateKey 'default' | 'unavailable' | 'filtered' | 'hovered' | 'selected' | 'loading' | 'error' type
SeatStylesMap Partial<Record<BuiltinSeatStateKey, ISeatStyle>> & { [customKey: string]: ICustomSeatStyle | ISeatStyle | undefined } type
IBasicSeatStyle { size; color; seatName?; stroke?; imageId?; shadow? } interface
ISeatStyle IBasicSeatStyle & { accessible?: IBasicSeatStyle } interface
ICustomSeatStyle { tint?; svg?; imageId?; className?; priority?; blockInteraction?; keepSeatName?; render? } interface
ISeatStateRenderArgs { seat: ISeat; stateKey: string; style: ICustomSeatStyle; sizePx: number } interface
IRendererTheme.seatStyles seatStyles?: SeatStylesMap theme field
IRendererTheme.images images?: { [id: string]: string } (named SVG/image assets resolved by imageId) theme field
  • Seat States & Hold Types – the backend hold-type lifecycle and data model (state / holdType on the booking service). That article covers how seats move through their lifecycle server-side; this one covers how to visualize and apply states client-side via the renderer SDK.
  • Section States & Styling – the equivalent concept applied to section outlines (highlighted, selected, unavailable, filtered).
  • Initializing the SDK – install @seatmap.pro/renderer, construct a renderer, and load an event before applying any custom states.