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:
publicKeyis required at runtime forSeatmapBookingRenderer; the constructor throws'Public key is undefined'if it is missing.setSeatsStatemust be called after seats exist – that is, afterloadEventresolves (or from inside anonSchemaDataLoadedcallback). 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
reservedthensoldto the same seat leaves it assold. - 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
blockInteractiontruthy. 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 toISeatStyle(the richer type, which adds theaccessibleoverride). 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 fullISeatStyle. Theundefinedin the union is required by TypeScript when mixing aPartial<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.
tintoverlays a filled circle in a CSS color over the seat footprint. (For built-in keys styled withISeatStyle,coloris the seat fill.) - Size. A custom state has no independent size; it shares the
defaultseat footprint (seatStyles['default']?.size, fallback10).ISeatStateRenderArgs.sizePxreports the resolved value. - Stroke. Available on
IBasicSeatStyle/ISeatStyle(width,color,align). Use a fullISeatStyleentry 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; setkeepSeatName: falseto suppress it.seatNametypography (font,color) is part of the built-inISeatStyle. - 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
ICustomSeatStylebut 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
selectedon the canvas and still carry a custom-state overlay on top. default.sizealso 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:
- An image drawn directly on the seat (via
imageIdon the seat style, or anonBeforeSeatDrawoverride) 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. - 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.
- Setting the image per-seat through
onBeforeSeatDrawreturns a freshISeatStyle, which replaces the whole style and drops the price/category color you set withsetSeatsCategory.
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.
setSeatsStatenever touchesseat.special.categoryor the category-color map, so a seat keeps the color you assigned withsetSeatsCategory. - The image fills the seat. The overlay draws the state’s
svg/imageIdat 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 (
keepSeatNamedefaults totrue); setkeepSeatName: falseonly when you want the icon to replace the number (the lock onsold, 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:
- The seat has an assigned state key.
seatStyles[stateKey]exists and hasblockInteractiontruthy.
Therefore:
- A seat with no assignment is never blocked.
- A seat whose style lacks
blockInteraction(or has it falsy) is not blocked. - If the
seatStylesmap 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:
reserved–tint,keepSeatName: true, animated pulse ring (SMIL<animate>onrandstroke-opacity, 1.4 s).held–tint,keepSeatName: true, fading ring (SMIL<animate>onstroke-opacity, 2.2 s).vip–tint,keepSeatName: true, rotating dashed ring (SMIL<animateTransform>rotate, 2.4 s).sold–tint,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 ansvg/imageId, an optionaltintlayer, 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 viasetSeatsCategory, and built-in state overrides inseatStyles. 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(fallback10) 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 callzoomToSection(sectionId, { focus: true }). - Stage independence. The overlay reads only stage-agnostic state – the viewport transform, the host element,
devicePixelRatio, andtheme.imagesforimageIdresolution. 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
setSeatsStateafterloadEventresolves (or fromonSchemaDataLoaded). - Wrong key / not in
seatStyles. A seat only renders an overlay when its assigned key has a matchingseatStylesentry. Verify the key string passed tosetSeatsStateexactly matches a key intheme.seatStyles.
States not clearing.
clearSeatsStateclears 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 |
Related
- Seat States & Hold Types – the backend hold-type lifecycle and data model (
state/holdTypeon 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.