Render a seat map
The minimum SDK call — mount the Seats chart inside a div, listen for selections.
Render a seat map
The smallest useful Seats integration is two steps: drop a <div>
container into your page and call SeatBuilder.render with your public
key, the event key, and the container id.
1. Add a container
Reserve a sized region of the page for the chart. The SDK fills the container — give it explicit width and height (or constrain it with flex / grid) so Konva can compute the canvas viewport. (Skip this if you're using the React component below — it renders its own container.)
<div id="seats-container" style="width: 800px; height: 600px;"></div>2. Call render
Choose the integration style that matches your stack: a <script> tag
needs no build step, npm suits bundler-driven apps, and the React
component wraps render() for you.
Script tag
<script src="https://seatbuilder.org/sdk/latest/seatbuilder.iife.min.js"></script>
<script>
const chart = SeatBuilder.render({
publicKey: 'pk_live_xxx',
eventKey: 'evt_xxx',
container: 'seats-container',
apiUrl: 'https://seatbuilder.org',
maxSelectedObjects: 4,
onSelectionChange: ({ selection }) => {
// selection.objects is the full current selection;
// selection.byCategory totals seats per category key.
console.log('selected', selection.objects.map((o) => o.label));
console.log('per category', selection.byCategory);
const canCheckout = !selection.pending && selection.count > 0;
document.querySelector('#checkout').disabled = !canCheckout;
},
});
</script>npm
npm install @seatbuilder/sdkimport SeatBuilder from '@seatbuilder/sdk';
const chart = SeatBuilder.render({
publicKey: 'pk_live_xxx',
eventKey: 'evt_xxx',
container: 'seats-container',
apiUrl: 'https://seatbuilder.org',
maxSelectedObjects: 4,
onSelectionChange: ({ selection }) => {
// selection.objects is the full current selection;
// selection.byCategory totals seats per category key.
console.log('selected', selection.objects.map((o) => o.label));
console.log('per category', selection.byCategory);
const canCheckout = !selection.pending && selection.count > 0;
document.querySelector<HTMLButtonElement>('#checkout')!.disabled = !canCheckout;
},
});React
'use client';
import { SeatBuilderChart } from '@seatbuilder/sdk/react';
<SeatBuilderChart
publicKey="pk_live_xxx"
eventKey="evt_xxx"
apiUrl="https://seatbuilder.org"
maxSelectedObjects={4}
style={{ height: 600 }}
onSelectionChange={({ selection }) => {
console.log('selected', selection.objects.map((o) => o.label));
console.log('can checkout', !selection.pending && selection.count > 0);
}}
/>The React component renders its own container <div> — no separate
#seats-container element needed. See
React & Next.js for the full recipe.
publicKey, eventKey, apiUrl, and container are required (the React
component manages its own container). apiUrl has no default — pass
https://seatbuilder.org; omitting it throws. The remaining options are
optional.
What you get
The rendered chart is interactive out of the box:
- Free seats highlight on hover.
- Click on a free seat fires
onObjectSelectedwith the seat label and a short-livedholdToken. Stash the token — you'll need it to promote the hold to a booking server-side. - Held / booked seats are not selectable and visually dim.
maxSelectedObjectscaps the buyer's selection; the SDK rejects clicks beyond the cap and firesonSelectionInvalid(no built-in toast — render your own feedback from that callback). Pass a barenumberfor a single total cap across all categories, or{ total?, perCategory? }to cap the total and individual categories — see Category-aware caps below.
The full e-commerce variant — adding-to-cart, confirming at checkout, recovering from expired holds — is documented in Hold seats and confirm at checkout.
On phones
In a small container the SDK switches to its compact layout: tap a section to
enter it, tap a seat to select it (tap it again to deselect) — a small card
next to the seat shows its details without covering the map — review the
selection in the tray at the bottom, and search by section, row or seat. Give the container a fixed height
between 360px and about 75vh so the rest of the page still scrolls, and drive
checkout from onSelectionChange.
Callbacks reference
onSelectionChange is the primary callback: it fires on every change with a
complete snapshot of the whole selection. onObjectSelected /
onObjectDeselected are the per-seat alternative — they fire once per seat
instead of handing you the full selection.
| Callback | Payload | Fires when |
|---|---|---|
onSelectionChange | { selection, added, removed, updated, reasons } | Any selection change. selection.objects is the full current selection; enable checkout when !selection.pending && selection.count > 0. |
onObjectSelected | { objectLabel, holdToken, categoryKey } | A single seat is selected and successfully held. Fires once per seat. |
onObjectDeselected | { objectLabel } | A single seat is deselected (its hold is released best-effort). |
onSelectionValid | { selectedObjectLabels } | The selection changes and at least one seat is held. selectedObjectLabels is the full current selection. |
onSelectionInvalid | { selectedObjectLabels, reason, categoryKey? } | The selection becomes empty or a hold fails. reason is 'max_selected', 'hold_expired', or 'hold_failed'. When a per-category cap fired, categoryKey names the capped category; it is omitted when the total cap fired. |
onChartRendered | { chartData } | The initial chart render completes (fires once). |
const chart = SeatBuilder.render({
publicKey: 'pk_live_xxx',
eventKey: 'evt_xxx',
container: 'seats-container',
apiUrl: 'https://seatbuilder.org',
maxSelectedObjects: 4,
onSelectionValid: ({ selectedObjectLabels }) => {
// The complete current selection, e.g. ['A-12', 'A-13'].
console.log('Selected so far:', selectedObjectLabels);
},
onSelectionInvalid: ({ reason, categoryKey }) => {
if (reason === 'hold_expired') promptReselect();
if (reason === 'max_selected' && categoryKey) {
// A per-category cap fired — tell the buyer which one.
console.log('Reached the limit for category', categoryKey);
}
},
});Category-aware caps
maxSelectedObjects accepts either a bare number (a single total cap)
or an object with total and/or perCategory. The perCategory keys
match the category.key values from your chart. When both total and
perCategory are set, both are enforced. A general-admission area
counts its quantity as N toward both caps (a hold of 50 counts as 50).
const chart = SeatBuilder.render({
publicKey: 'pk_live_xxx',
eventKey: 'evt_xxx',
container: 'seats-container',
apiUrl: 'https://seatbuilder.org',
// At most 6 seats total, with no more than 2 in the `vip` category.
maxSelectedObjects: { total: 6, perCategory: { vip: 2 } },
onSelectionInvalid: ({ reason, categoryKey }) => {
if (reason === 'max_selected') {
console.log(
categoryKey
? `Limit reached for category ${categoryKey}`
: 'Total seat limit reached',
);
}
},
});When a per-category cap is the one that blocks the click,
onSelectionInvalid carries categoryKey; when the total cap blocks
it, categoryKey is omitted.
Configuration reference
| Option | Type | Required | Default | Notes |
|---|---|---|---|---|
publicKey | string | yes | — | A pk_* publishable key — safe to expose in the browser. |
eventKey | string | yes | — | The event to render seat availability for. |
container | HTMLElement | string | yes | — | A DOM element or its id. Must have an explicit height. |
apiUrl | string | yes | — | Base URL of your SeatBuilder API. Required since @seatbuilder/sdk 1.0.0. |
mobile | 'auto' | 'always' | 'never' | no | 'auto' | Compact (mobile) layout chosen from the container's size: under mobileBreakpoint wide, or under 900px wide and 480px tall. |
mobileBreakpoint | number | no | 640 | Container width (px) below which 'auto' picks compact. |
mobileTray | boolean | no | true | Built-in selected-seat tray in compact. Turn off if you show your own basket. |
onLayoutChange | (e) => void | no | — | { layout } on the initial choice and every switch. |
maxSelectedObjects | number | { total?, perCategory? } | no | unlimited | Caps the buyer's selection. A bare number is the total cap; { total?, perCategory? } adds per-category caps (perCategory keys match category.key, both enforced together, GA quantity counts as N). Clicks beyond a cap surface a toast and fire onSelectionInvalid with reason: 'max_selected' (and categoryKey when a per-category cap fired). |
showSeatLabels | boolean | no | false | Show per-seat number labels (still hidden below the ~0.43× zoom threshold). |
Instance API
SeatBuilder.render() returns a ChartInstance for imperative control:
| Member | Signature | Purpose |
|---|---|---|
holdToken | string | The session hold token. Share it with your backend to book the held seats. |
clearSelection | () => void | Deselect all seats and release their holds. |
selectObjects | (labels: string[]) => Promise<void> | Programmatically select (and hold) seats by label. |
getSeatInfo | (label: string) => SeatInfo | null | O(1) lookup of a seat or GA area's position, category and live status. null before render or for unknown labels. |
getSelectedObjects | () => SelectionSnapshot | The current selection snapshot (same shape onSelectionChange delivers). |
destroy | () => void | Tear down the chart and release all resources. |
fitVenue / focusSection / focusSeat / focusSeats / fitSelection | — | Camera control — see Camera below. |
Camera
| Method | Description |
|---|---|
fitVenue() | Animate to the whole venue (returns to the overview if drilled into a section). |
focusSection(sectionKey) | Enter a section and frame it. |
focusSeat(label) | Bring one seat into view, moving as little as possible. |
focusSeats(labels) | Frame a group of seats (overview when they span several sections). |
fitSelection() | Frame the current selection; no-op when empty. |