React & Next.js
Render the seat map with the <SeatBuilderChart> React component.
React & Next.js
@seatbuilder/sdk/react ships a <SeatBuilderChart> component that wraps
SeatBuilder.render() for you — no manual useEffect/container-ref
plumbing required.
1. Install
npm install @seatbuilder/sdkRequires React ≥ 18. konva and zod are installed automatically as
dependencies of @seatbuilder/sdk.
2. Basic component
'use client';
import { useRef } from 'react';
import { SeatBuilderChart } from '@seatbuilder/sdk/react';
import type { ChartInstance } from '@seatbuilder/sdk';
export function SeatMap({ eventKey }: { eventKey: string }) {
const chart = useRef<ChartInstance | null>(null);
return (
<SeatBuilderChart
ref={chart}
publicKey="pk_live_..."
eventKey={eventKey}
apiUrl="https://seatbuilder.org"
onObjectSelected={(e) => console.log('held', e.objectLabel)}
style={{ height: 600 }}
/>
);
}- Safe to render from a Server Component tree; the chart mounts on the client.
- Changing an
on*callback,onReady,classNameorstylenever re-mounts the chart; changing a config prop (anything else, e.g.eventKey) destroys it and renders a new one. - Give the component a height (
styleorclassName) — a 0-height container renders nothing.
3. Next.js App Router
SeatBuilderChart only touches the DOM inside a useEffect, so it
server-renders safely as long as the 'use client' boundary is on the
child component, not the page. You do not need
dynamic(() => import(...), { ssr: false }).
// app/events/[eventKey]/page.tsx
import { SeatMap } from './seat-map';
export default async function EventPage({
params,
}: {
params: Promise<{ eventKey: string }>;
}) {
const { eventKey } = await params;
return (
<main>
<h1>Pick your seats</h1>
<SeatMap eventKey={eventKey} />
</main>
);
}// app/events/[eventKey]/seat-map.tsx
'use client';
import { useRef } from 'react';
import { SeatBuilderChart } from '@seatbuilder/sdk/react';
import type { ChartInstance } from '@seatbuilder/sdk';
export function SeatMap({ eventKey }: { eventKey: string }) {
const chart = useRef<ChartInstance | null>(null);
return (
<SeatBuilderChart
ref={chart}
publicKey="pk_live_..."
eventKey={eventKey}
apiUrl="https://seatbuilder.org"
style={{ height: 600 }}
/>
);
}4. Props reference
SeatBuilderChartProps is SdkConfig minus container (the component
manages its own container <div>), plus onReady, className, and
style.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
publicKey | string | yes | — | Publishable API key (pk_live_... or pk_test_...). |
eventKey | string | yes | — | Event identifier. Must reference an event whose chart is published. |
apiUrl | string | yes | — | Base URL of the SeatBuilder API — https://seatbuilder.org. Required since 1.0.0; the previous https://api.seats.io default was removed. |
maxSelectedObjects | number | { total?, perCategory? } | no | unlimited | Cap on concurrent selections. A bare number is the total cap (≡ { total }). Pass { total?, perCategory? } for category-aware caps — perCategory keys match category.key and both are enforced when set together. A GA-area quantity counts as N toward both caps. Clicks beyond a cap fire onSelectionInvalid (reason: 'max_selected'). |
onObjectSelected | (e) => void | no | — | Payload { objectLabel, holdToken, categoryKey, quantity? }. Fires when a seat is selected and the hold API call has succeeded. |
onObjectDeselected | (e) => void | no | — | Payload { objectLabel }. Fires when a seat is deselected and the release API call has succeeded. |
onSelectionValid | (e) => void | no | — | Payload { selectedObjectLabels }. Fires when the selection changes and is within maxSelectedObjects. |
onSelectionInvalid | (e) => void | no | — | Payload { selectedObjectLabels, reason, categoryKey? } (reason is 'max_selected', 'hold_expired', or 'hold_failed'). Fires when a selection exceeds a limit, a hold expires, or a hold fails. categoryKey is set only when a per-category cap fired. |
onChartRendered | (e) => void | no | — | Payload { chartData }. Fires once, when the initial render completes (chartData is the chart's currently-published version). |
showSeatLabels | boolean | no | false | Show seat number labels on the map. Labels are always hidden below ~0.43× zoom regardless of this setting. |
holdDurationSeconds | number | no | server default (900s) | Browse-hold lifetime in seconds, passed as ttlSeconds on the initial /hold. No silent client default — the field is simply absent from the /hold body when unset. |
showHoldCountdown | boolean | no | false | When true, re-enables the built-in 2-minute warning banner + auto-deselect timer. |
showStatusLegend | boolean | no | true | Shows a small seat-status legend (Selected / On hold / Sold / Not for sale) at the top-left of the map. Set false to hide it for a minimal embed. |
selectionMode | 'hold' | 'select' | no | 'hold' | 'hold' is the buyer purchase flow; 'select' is operator selection-only, with no /hold round-trip. |
language | string | no | 'en' | BCP-47 code selecting a built-in message catalog ('en' | 'vi'). Unknown codes fall back to 'en'. |
messages | Partial<SdkMessages> | no | — | Per-key string overrides merged over the selected language catalog. |
onReady | (chart: ChartInstance | null) => void | no | — | Called with the live instance after render, and with null on teardown. |
className | string | no | — | CSS class applied to the component's container <div>. |
style | CSSProperties | no | — | Inline styles applied to the container <div> — give it an explicit height. |
5. Re-mount rules and the ref API
- Callback props (
onObjectSelected,onSelectionValid, etc.) are read through a ref — changing them never re-mounts the chart. onReady,className,stylenever re-mount the chart either.- Config props (everything else:
eventKey,apiUrl,maxSelectedObjects, ...) are compared structurally; changing a config prop destroys the current chart and renders a new one.
Attach a ref to read the imperative ChartInstance. ref.current is
null until the chart mounts (after the first client render/effect), so
read it in event handlers or use onReady rather than during render:
| 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. |
refreshStatus | () => Promise<void> | Re-fetch the server status snapshot and repaint every seat in place. |
destroy | () => void | Tear down the chart and release all resources. |
Do not call destroy() yourself — unmounting <SeatBuilderChart>
already calls it for you in its cleanup effect.
6. Checkout hand-off
Read the hold token off the ref when the buyer submits checkout, and POST it to your backend to promote the holds to a booking:
async function handleCheckout() {
const holdToken = chart.current?.holdToken;
if (!holdToken) return;
await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ holdToken }),
});
}See Hold seats and confirm at checkout for the full server-side extend/book flow.
Render a seat map
The minimum SDK call — mount the Seats chart inside a div, listen for selections.
Hold and book seats at checkout
The deferred-countdown e-commerce flow — instant cross-user lock on select, a short browse TTL, extend to a payment window at checkout, an integrator-owned countdown, and graceful handling of expired or contested holds.