---
title: "Build a seating chart in React"
description: "Build an interactive React seating chart with the @seatlayer/react npm package, from install to selection, holds, and checkout handoff."
---

This tutorial builds an interactive seating chart in React with the
`@seatlayer/react` npm package: a live seat map with selection, temporary holds,
and a checkout handoff to your own backend. It uses the released `SeatPicker`
component, so the map, selection tray, hold countdown, and mobile layout come
with it — you write the parts that belong to your product.

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](https://seatlayer.io/pricing/) for what happens after that,
or the [seat map SDK and API overview](https://seatlayer.io/developers/) for how
the pieces fit together.

## 1. Install the package

<PackageManagers pkg="@seatlayer/react@^0.64.0" />

For script-tag, browser ESM, plain JavaScript, Vue, and Angular routes, see the
[install options for all frameworks](/buyer-sdk/install).

## 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.

```tsx title="Checkout.tsx"
import { SeatPicker } from "@seatlayer/react";

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

<Aside type="caution" title="Platform embeds require browser audience access">
  The event key is not a credential. Every Platform/SDK picker must provide
  `buyerAccessTokenProvider`, backed by your authenticated server and bound to
  the exact event and browser origin. The example above focuses on rendering;
  step 3 adds the provider, and the complete token-minting flow is in the
  [Platform SDK quickstart](/start/quickstart). Managed public/unlisted event
  pages retain anonymous access.
</Aside>

## 3. Authorize the browser

`buyerAccessTokenProvider` is called with a reason and returns a 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.

```tsx title="Checkout.tsx"
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="ev_9f3a"
      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.

## 4. React to selection and holds

The component takes the same options and callbacks as the
[SeatPicker reference](/buyer-sdk/seat-picker), 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.

```tsx title="Checkout.tsx"
const [seats, setSeats] = useState([]);

return (
  <SeatPicker
    event="ev_9f3a"
    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:

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

<Steps>
  <Step title="Hand off only holdId">
    Your checkout route receives the hold identity and your own cart context,
    never a secret key or trusted price.
  </Step>
  <Step title="Build the trusted order on your server">
    It inspects the active hold, calculates the charge from its returned items,
    and runs your payment workflow.
  </Step>
  <Step title="Start a visible timer from server time">
    Count down from server `expiresAt`, not a locally invented duration.
  </Step>
</Steps>

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](/buyer-sdk/holds-and-checkout).

## 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.

```js title="headless-chart.js"
import { SeatingChart } from "@seatlayer/js";

const chart = new SeatingChart({
  container: "#chart",
  event: "ev_9f3a",
  onSelectionChange: (seats) => {
    updateYourSelectionUI(seats);
  },
});

await chart.render();
```

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](/buyer-sdk/seating-chart).

If you are weighing that against building the map yourself, the
[JavaScript seating chart guide](https://seatlayer.io/guides/javascript-seating-chart/)
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.
- [ ] Platform embeds use `buyerAccessTokenProvider`; the `bse_…` token stays in
      memory.
- [ ] Narrow mobile and keyboard behavior are usable.

## Questions

### Which React seating chart library should I use?

It depends on how much of the buyer experience you want to own. `SeatPicker`
gives you the complete flow — map, confirmation, pricing, selection tray, holds,
expiry, success state, mobile layout, and optional 3D — and is the fastest
production route. `SeatingChart` gives you only the live map canvas, and your
product builds the cart, CTA, timer, and states around it. Building on a generic
canvas or charting library instead means you also own venue geometry, live
inventory, hold expiry, and concurrency, which is the part that is hard to keep
correct.

### Is there a React seat picker on npm?

Yes — `@seatlayer/react`, pinned in this tutorial at `^0.64.0`. It provides
native `SeatPicker` and `SeatingChart` components, and re-exports the
framework-agnostic `SeatPickerWidget` modal and `attachPickerFrame` helper from
`@seatlayer/js`.

### Is there a seats.io alternative for React?

SeatLayer publishes a native React SDK for reserved seating, covering the
complete picker and the headless chart on the same event and live inventory
model. For a feature-by-feature view, read the
[seats.io alternative](https://seatlayer.io/seats-io-alternative/) page.

Continue to the [Quickstart](/start/quickstart), or learn the
[core hold and booking model](/start/how-it-works).