SeatBuilderSeatBuilder Docs
Recipes

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/sdk

Requires 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, className or style never 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 (style or className) — 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.

PropTypeRequiredDefaultDescription
publicKeystringyes—Publishable API key (pk_live_... or pk_test_...).
eventKeystringyes—Event identifier. Must reference an event whose chart is published.
apiUrlstringyes—Base URL of the SeatBuilder API — https://seatbuilder.org. Required since 1.0.0; the previous https://api.seats.io default was removed.
maxSelectedObjectsnumber | { total?, perCategory? }nounlimitedCap 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) => voidno—Payload { objectLabel, holdToken, categoryKey, quantity? }. Fires when a seat is selected and the hold API call has succeeded.
onObjectDeselected(e) => voidno—Payload { objectLabel }. Fires when a seat is deselected and the release API call has succeeded.
onSelectionValid(e) => voidno—Payload { selectedObjectLabels }. Fires when the selection changes and is within maxSelectedObjects.
onSelectionInvalid(e) => voidno—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) => voidno—Payload { chartData }. Fires once, when the initial render completes (chartData is the chart's currently-published version).
showSeatLabelsbooleannofalseShow seat number labels on the map. Labels are always hidden below ~0.43× zoom regardless of this setting.
holdDurationSecondsnumbernoserver 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.
showHoldCountdownbooleannofalseWhen true, re-enables the built-in 2-minute warning banner + auto-deselect timer.
showStatusLegendbooleannotrueShows 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.
languagestringno'en'BCP-47 code selecting a built-in message catalog ('en' | 'vi'). Unknown codes fall back to 'en'.
messagesPartial<SdkMessages>no—Per-key string overrides merged over the selected language catalog.
onReady(chart: ChartInstance | null) => voidno—Called with the live instance after render, and with null on teardown.
classNamestringno—CSS class applied to the component's container <div>.
styleCSSPropertiesno—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, style never 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:

MemberSignaturePurpose
holdTokenstringThe session hold token. Share it with your backend to book the held seats.
clearSelection() => voidDeselect 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() => voidTear 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.

React & Next.js — SeatBuilder Docs