Skip to content

Add a seat map to a React app

Follow a step-by-step React tutorial to install SeatPicker, render live seats, authorize buyers, handle selection, and pass a hold to checkout.

Updated View as Markdown

To add a seat map to a React app, install @seatlayer/react, render the SeatPicker component with an event key and a public key, and pass the hold it returns to your own checkout. The five numbered steps below do exactly that: install, mount, authorize the audience, react to selection and holds, then hand off to checkout.

This tutorial walks from an empty React checkout page to a working SeatPicker with live selection, temporary holds, private-audience authorization, and a secure checkout handoff. The picker already includes the map, selection tray, hold countdown, and mobile layout; the steps below connect those pieces to your application.

Looking for package exports, React requirements, module formats, or the headless component contract? Use the @seatlayer/react package reference instead.

Test mode is free with no time limit and no card, so you can complete every step below before a live account exists. See SeatLayer pricing for what happens after that, or the seat map SDK and API overview for how the pieces fit together.

1. Install the package

npm i @seatlayer/react@0

For package requirements and the complete export directory, see the React SDK reference. For script-tag, browser ESM, plain JavaScript, Vue, and Angular routes, see the install options for all frameworks.

2. Render the seating chart

The React wrapper exports SeatPicker. Give it an event key and an explicit size. The SDK is container-responsive, so its mount element needs a definite width and height.

Checkout.tsxtsx
import { SeatPicker } from "@seatlayer/react";

export function Checkout() {
  return (
    <SeatPicker
      event="<YOUR_EVENT_KEY>"
      publicKey="<YOUR_PUBLIC_KEY>"
      style={{ width: "100%", height: 640 }}
      onCheckout={(_, __, handoff) => {
        beginCheckout(handoff.holdId);
      }}
    />
  );
}

3. Authorize private audiences

Skip this step when every buyer sees only Public sale. For login, presale, partner, or channel inventory, buyerAccessTokenProvider is called with a reason and returns a scoped token and expiry from your own authenticated backend. Keep the function outside the component (or in a useCallback) so it is not recreated on every render.

Checkout.tsxtsx
import { SeatPicker } from "@seatlayer/react";

async function getSeatLayerBuyerAccess({ reason }) {
  const response = await fetch("/api/seatlayer/buyer-access", {
    method: "POST",
    credentials: "same-origin",
    cache: "no-store",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ reason }),
  });
  if (!response.ok) throw new Error("Unable to open seat selection");
  return response.json();
}

export function Checkout() {
  return (
    <SeatPicker
      event="<YOUR_EVENT_KEY>"
      style={{ width: "100%", height: 640 }}
      buyerAccessTokenProvider={getSeatLayerBuyerAccess}
      onCheckout={(_hold, _seats, handoff) => {
        startCheckout({ holdId: handoff.holdId });
      }}
    />
  );
}

The bse_… token stays in memory. No sk_… credential belongs in a client bundle. If both publicKey and a provider/token are supplied, the explicit buyer credential takes precedence and the SDK never falls back to anonymous Public sale.

4. React to selection and holds

The component takes the same options and callbacks as the SeatPicker reference, passed as props. Two are useful almost immediately:

  • onSelectionChange: (seats) => void, fires whenever the selection changes.
  • onHoldChange: (hold, seats, handoff) => void, fires when a hold is created, restored, extended, partially released, or released.
Checkout.tsxtsx
const [seats, setSeats] = useState([]);

return (
  <SeatPicker
    event="<YOUR_EVENT_KEY>"
    style={{ width: "100%", height: 640 }}
    buyerAccessTokenProvider={getSeatLayerBuyerAccess}
    onSelectionChange={setSeats}
    onError={reportPickerError}
    onCheckout={(_hold, _seats, handoff) => {
      startCheckout({ holdId: handoff.holdId });
    }}
  />
);

SeatPicker already renders its own selection tray and hold countdown, so treat this state as context for the rest of your page rather than a cart you have to build.

5. Hand off to your checkout

A hold temporarily protects inventory while a buyer completes checkout. It is not a sale. The Buyer SDK creates the hold; your server inspects and books it.

The third onCheckout argument is the handoff:

interface CheckoutHandoff {
  holdId: string;
  expiresAt: number;
  currency: string;
  lineItems: CheckoutLineItem[];
  total: number;
}

Hand off only holdId

Your checkout route receives the hold identity and your own cart context, never a secret key or trusted price.

Build the trusted order on your server

It inspects the active hold, calculates the charge from its returned items, and runs your payment workflow.

Start a visible timer from server time

Count down from server expiresAt, not a locally invented duration.

The browser handoff is excellent display context, but payment still uses a fresh server-side inspection. The complete flow, including release and restore, is in holds and checkout handoff.

6. Optional: go headless with SeatingChart

@seatlayer/react also exports SeatingChart, the headless seating canvas without SeatLayer’s cart/tray chrome. Reach for it only when your application owns totals, tier/GA controls, the checkout button, countdown, and buyer messages.

CustomSeatMap.tsxtsx
import { SeatingChart } from "@seatlayer/react";

export function CustomSeatMap() {
  return (
    <SeatingChart
      event="<YOUR_EVENT_KEY>"
      publicKey="<YOUR_PUBLIC_KEY>"
      style={{ width: "100%", height: 640 }}
      onSelectionChange={(seats) => updateYourSelectionUI(seats)}
    />
  );
}

Choose SeatPicker unless your application has a clear reason to own selection controls, confirmation, hold timing, pricing presentation, and mobile behavior. Full options are in the SeatingChart headless reference.

If you are weighing that against building the map yourself, the JavaScript seating chart guide walks through what a hand-built chart still leaves you owning.

Before you ship

  • The package loads without browser errors.
  • The mount container has an explicit usable size.
  • A test event renders.
  • Selection changes appear immediately.
  • Checkout produces a holdId.
  • No sk_… credential exists in the client bundle.
  • Public Platform embeds use publicKey; scoped private audiences use a provider/token, and every bse_… bearer stays in memory.
  • Narrow mobile and keyboard behavior are usable.

Troubleshoot the walkthrough

Why is the seat map blank or collapsed?

Give the component a definite height, then check that the event key, key mode, and exact registered browser origin match. A responsive picker cannot infer a usable height from a collapsed parent.

When should I use publicKey or buyerAccessTokenProvider?

Use the mode-matched publishable publicKey for an event where every buyer sees Public sale. Use a backend-backed provider for login, presale, partner, or channel inventory. If both are present, the explicit buyer credential wins.

What should the React app send to checkout?

Send the opaque holdId plus your own cart context. Your backend inspects the active hold, derives trusted line items and pricing, runs payment, and books with a stable bookingRef; do not trust a browser-submitted amount.

Runnable examples

Two complete applications put the steps above together, ready to clone:

  • seatlayer-react-example: Vite and React, with the headless SeatingChart, best available, holds, and a checkout handoff.
  • seatlayer-nextjs-example: the same flow on Next.js 15 App Router, plus an /api/hold server route that shows where the trusted half belongs.

Both read an .env.local with your event key and public key, and both carry one-click deploy buttons in their READMEs.

Continue to the @seatlayer/react package reference, the Quickstart, or the core hold and booking model.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close