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@0yarn add @seatlayer/react@0pnpm add @seatlayer/react@0bun add @seatlayer/react@0For 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.
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.
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.
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.
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 everybse_…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/holdserver 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.