The official React components for SeatLayer reserved seating. Drop an interactive seating chart or a complete seat picker into a ticketing app, show live seat availability, and let buyers take temporary holds on the inventory they choose.
Everything is a real React component with a typed imperative handle: the browser selects and holds, and your trusted server books the hold.
SeatLayer React SDK on npm · React seat-map documentation · SeatLayer SDK and API overview · Buyer seat-map demo · SeatLayer JavaScript seat map SDK · SeatLayer Vue seat map SDK · SeatLayer Angular seat map SDK · SeatLayer React Native SDK · SeatLayer AI Toolkit
SeatingChart: the lower-level interactive chart, mounted into a plain<div>your styles own.SeatPicker: the complete buyer experience: map, legend, tray, pricing, and the checkout hand-off.SeasonPicker: fixed-inclusion Season selection and returning-holder intent with the same typed lifecycle and imperative-handle conventions.SeatManager: the organizer control room, for dashboards that monitor and block live inventory.EmbeddedDesigner: a hosted chart Designer inside your own application.SeatPickerWidgetandattachPickerFrame: the framework-agnostic modal and iframe helpers, re-exported so a React host depends on this package alone.- TypeScript declarations for ESM (
dist/index.d.ts) and CommonJS (dist/index.d.cts), plus the@seatlayer/react/managersubpath.
- React 17 or newer (declared as a peer dependency).
- A browser DOM. The chart is created inside an effect, and this package ships
no
'use client'banner of its own. In the Next.js App Router, mark the component that imports it.
npm install @seatlayer/reactimport { useRef } from 'react';
import { SeatingChart, type SeatingChartHandle } from '@seatlayer/react';
export function Checkout() {
const chart = useRef<SeatingChartHandle>(null);
return (
<SeatingChart
ref={chart}
event="ev_9f3a"
publicKey="pk_live_your_publishable_key"
style={{ width: '100%', height: 520 }}
onSelectionChange={(seats) => console.log('selected', seats)}
onHold={({ holdId }) => bookOnYourServer(holdId)}
/>
);
}Drive it imperatively through the ref:
const hold = await chart.current?.hold(); // hold the selection (null on conflict)
const best = await chart.current?.bestAvailable(4); // auto-pick 4 seats and hold them
const restored = await chart.current?.resumeHold(savedHoldId);
await chart.current?.releaseLabels(['A-12']); // keep the remainder held
await chart.current?.release(); // release the current holdconst season = useRef<SeasonPickerHandle>(null);
<SeasonPicker
ref={season}
season="sea_2027"
buyerAccessToken={session.token}
onContinue={(handoff) => continueOnYourServer(handoff.operationId)}
/>
await season.current?.holdSameSeat(['A-1'], 'sop_checkout_1183');The browser handoff is opaque and price-free. Its pricingAuthority: "host"
and authoritativeAmountIncluded: false flags mean trusted server code must
apply the package price, tax, and payment decision before booking the
identity-only allocation. SeasonPicker is supported in @seatlayer/react
0.72.0 and newer.
For the complete buyer experience (map, legend, priced tray, and the checkout
hand-off), render SeatPicker instead:
import { SeatPicker } from '@seatlayer/react';
export function Tickets() {
return (
<SeatPicker
event="ev_9f3a"
publicKey="pk_live_your_publishable_key"
style={{ width: '100%', height: 640 }}
selectionValidators={[
{ type: 'minimumSelectedPlaces', minimum: 2 },
{ type: 'consecutiveSeats' },
{ type: 'noOrphanSeats' },
]}
onCheckout={(hold, seats, handoff) => myServerTakesPayment(handoff)}
/>
);
}The full-experience SeatPicker exposes the canonical safe widget controls through
SeatPickerHandle:
| React ref method | Contract |
|---|---|
close() |
Logically close the picker. The host should still unmount the React component. |
getSelection() |
Read the current priced selection. |
selectObjects(), deselectObjects(), clearSelection() |
Control selection by engine id or public label. |
selectCategories(), deselectCategories() |
Control every selectable object in named categories. |
setSelectableObjects(), setMaxSelection(), getSelectionValidity() |
Update selection policy and inspect exact-count validity. |
bestAvailable(qty, categoryKey?, options?) |
Pick and hold server-authoritative seats; options support preferPremium and zoneId. |
getCurrentHold(), resumeHold(), removeHeldTicket(), release() |
Inspect and manage the active hold. |
setMapTheme(), setEventDetailsHidden(), setPricing() |
Update host-owned presentation without remounting or losing a hold. |
isColorblindSafe(), setColorblindSafe() |
Read and update the shared buyer accessibility preference. |
getViewMode(), setViewMode() |
Read and update the 2D map projection. |
getBuyerView(), setBuyerView() |
Switch between the map and interactive 3D seat views, including optional camera intent. |
refreshAccess() |
Re-acquire a private buyer-access session. |
destroy() |
Intentionally omitted: React owns teardown through component unmount. |
Set webMcp on SeatPicker to let a compatible in-browser assistant describe
the event, find seats, select them on the map, and read the current selection:
import { SeatPicker } from '@seatlayer/react';
<SeatPicker
event="ev_9f3a"
publicKey="pk_live_your_publishable_key"
webMcp
style={{ width: '100%', height: 640 }}
onCheckout={(hold) => {
void continueCheckoutOnYourServer(hold.holdId).catch(showCheckoutError);
}}
/>continueCheckoutOnYourServer and showCheckoutError are supplied by your
application. The first owns the async server request; .catch(...) surfaces a
rejected checkout.
WebMCP is off by default and inert in browsers without the API. Use
webMcp={{ holds: true }} only when the assistant should also be able to place
a temporary hold. The tools do not take payment or book seats; see the
buyer WebMCP seat-selection guide
for iframe origin controls and the complete tool contract.
Extends the vanilla SDK options minus container (the component owns its own mount).
| Prop | Type | Notes |
|---|---|---|
event |
string |
Required. The event key, e.g. ev_9f3a. |
publicKey |
string? |
Publishable browser key for public Platform events. Configure the browser origin in SeatLayer. |
apiBase |
string? |
API origin. Defaults to the SeatLayer production API. |
maxSelection |
number? |
Max seats selectable at once (default 10). |
selectedObjects |
string[]? |
Initial object ids or public labels. |
selectableObjects |
string[] | null |
Buyer-selectable allow-list. |
numberOfPlacesToSelect |
number? |
Exact count required for a valid selection. |
selectionValidators |
PickerSelectionValidator[]? |
Minimum, consecutive-seat, and no-orphan guards. |
onSelectionChange |
(seats) => void |
Fires when the selection changes. |
onSelectionValidityChange |
(state) => void |
Rule state and typed violations after selection changes. |
onHold |
(result) => void |
Fires when seats are held; hand holdId to your server. |
onHoldRestored |
(result) => void |
Fires after resumeHold() verifies an active hold. |
onError |
(err) => void |
Fires on errors. |
className / style |
not applicable | Applied to the container element. |
Changing a callback prop does not rebuild the canvas. selectedObjects and
selectableObjects are initial values; use the imperative methods for later
changes. Exact count and validator props rebuild the chart.
The React app selects and holds inventory. Your trusted backend inspects and books the hold after payment or order validation.
- Never ship a SeatLayer secret key in browser code or in a bundled environment variable.
- A
pk_public key is designed for browser code. SeatLayer binds it to the configured origin and event; keep private-channel access behind a server-mintedbuyerAccessTokenProvider. - Send only the
holdIdand your normal checkout context to your backend. - Calculate the charge from server-inspected hold items, not from browser input.
- Reuse your stable order id as
bookingRefso a retried booking is idempotent. - Organizer surfaces take a short-lived, event-scoped
mse_browser grant, never a tenantsk_secret.
Read how the integration works before connecting checkout.
These components are a thin React layer over
@seatlayer/js, the
framework-agnostic browser runtime. The wrapper mounts a plain <div>, builds
the chart inside an effect, forwards every prop and callback, and exposes the
runtime's imperative handle through ref. Chart geometry, availability, and
holds all come from the SeatLayer API at runtime, so the same chart and event
inventory can be reused across supported web and mobile SDKs.
Callback props are read through a ref, so changing one never tears the chart down. Only the identity props listed above rebuild it.
Use SeatManager with a short-lived, event-scoped manage token minted by your
backend. The token prop accepts an mse_ browser grant; tenant sk_ secrets
are unsupported in React/browser code and must stay on your server. Keep the
grant in memory, never in a URL, browser storage, or logs. Monitor, Inspect,
Block/unblock, fullscreen, presence, exact configured
booked value, booking velocity, and the Booking momentum overlay are one
shared package surface, not host-owned tabs.
Block mode includes explicit multi-select category controls plus a searchable,
section-filtered blocked-inventory list for restoring specific seats to sale.
Select mode adds host-owned initial/programmatic selection, availability policy,
maximum and exact-count rules, and validity callbacks. Capability-gated
Categories and Tables tools change only the event snapshot; table mode changes
fail safely while affected inventory is held, booked, or blocked.
Filter Sections groups duplicate public labels, frames their combined inventory,
and reports the matched sections through the React callback and ref methods.
<SeatManager
ref={manager}
apiBase="https://api.seatlayer.io"
eventKey="ev_9f3a"
token={session.token}
tokenExpiresAt={session.expiresAt}
onTokenRefresh={() => mintManageSession()}
style={{ width: '100%', height: 'calc(100vh - 96px)' }}
/>Token rotation and tool changes preserve the live renderer, camera, selection, WebSocket, and DOM. The component responds to its own container, including compact embeds, and the buyer SDK remains unchanged when manager options are off.
EmbeddedDesigner gives an organizer the hosted Designer inside your React
application. Mint designerUrl from your backend, never from a browser using an
account secret key. The component verifies messages by iframe source, Designer origin,
and the chart/workspace ids you provide.
import { EmbeddedDesigner } from '@seatlayer/react';
export function VenueEditor({ session }: { session: {
designerUrl: string; chartId: string; workspaceId: string;
} }) {
return (
<EmbeddedDesigner
designerUrl={session.designerUrl}
expectedChartId={session.chartId}
expectedWorkspaceId={session.workspaceId}
style={{ width: '100%', height: 'calc(100vh - 96px)' }}
onSaved={({ chartId }) => refreshVenue(chartId)}
onPublished={({ chartId }) => refreshVenue(chartId)}
onClose={() => closeVenueEditor()}
onError={({ code, message }) => showError(code ?? message)}
onRequestRelaunch={async () => {
const next = await mintDesignerSession(session.chartId);
setDesignerUrl(next.designerUrl); // a new designerUrl recreates the iframe
}}
/>
);
}Changing designerUrl replaces the iframe, so a newly-minted fragment token never
continues an earlier session. See
the embedded Designer guide.
The default height: 'fill' is container-aware: give the container element a
definite height (a fixed-height block, flex-1 min-h-0, a resolved %) and the
Designer fills 100% of it, tracking size changes live via a ResizeObserver; leave
it content-sized (e.g. style={{ height: 'calc(100vh - 96px)' }} full-page) and it
fills the viewport instead. The result is clamped to minHeight (default 480) and
re-probed on resize. SDK-managed heights are applied with !important, so a host
theme's iframe { height: … } rule can't override them.
The component paints a branded loading skeleton inside its container until the
Designer reports ready, then removes it. On an expired session, identity
mismatch, iframe error, or a load timeout it shows a dark "Try again" card
with cause-specific copy. The skeleton honors prefers-reduced-motion.
| Prop | Type | Default | What it does |
|---|---|---|---|
showLoadingState |
boolean? |
true |
Render the built-in skeleton and error card. Set false to supply your own chrome. |
loadingTimeoutMs |
number? |
20000 |
Show the error card if ready never arrives within this window. |
onRequestRelaunch |
() => void |
none | Called by "Try again" and by automatic renewal (below). Mint a fresh session and set a new designerUrl (which recreates the iframe and returns to loading). When omitted, "Try again" reloads the current URL. |
autoRenewSession |
boolean? |
true¹ |
Silently renew the session before it expires and auto-recover once if an expiry error slips through. ¹Defaults true only when onRequestRelaunch is provided; a no-op without it. Set false for fully manual "Try again". |
All props are optional and additive: existing integrations keep working unchanged.
Designer sessions are short-lived by design: your backend mints a dse_
token (default 1 hour, up to 4 hours via expiresInSeconds) baked into
designerUrl. Choose a TTL that matches how long organizers actually edit: the
renewal below keeps even a multi-hour session alive, so you needn't over-provision.
Pass onRequestRelaunch that mints (and awaits) a fresh session and updates the
designerUrl state, and the component makes expiry a non-event:
- Silent proactive renewal: from each
ready'sexpiresAtit schedules an automatic relaunch shortly before the session lapses (~3 min ahead, or after 80% of the remaining life for a sub-15-minute TTL, never sooner than 30s afterready), re-armed on everyready. - Automatic expiry recovery: if an expiry error slips through anyway, it makes one automatic relaunch attempt before showing the "Try again" card.
The wrapper recreates the iframe whenever designerUrl changes, and
in-progress work is autosaved server-side, so relaunching is safe: the
organizer's chart is restored right where they left off.
export function VenueEditor({ chartId }: { chartId: string }) {
const [designerUrl, setDesignerUrl] = useState<string>();
useEffect(() => {
mintDesignerSession(chartId).then((s) => setDesignerUrl(s.designerUrl));
}, [chartId]);
if (!designerUrl) return null;
return (
<EmbeddedDesigner
designerUrl={designerUrl}
expectedChartId={chartId}
style={{ width: '100%', height: 'calc(100vh - 96px)' }}
// Mint a fresh session on renewal, expiry recovery, or "Try again":
onRequestRelaunch={async () => {
const next = await mintDesignerSession(chartId); // your backend, up to 4h TTL
setDesignerUrl(next.designerUrl); // swapping the URL recreates the iframe
}}
// autoRenewSession defaults to true because onRequestRelaunch is present.
/>
);
}Every method the runtime exposes to a wrapper, reachable through the ref:
hold · resumeHold · getCurrentHold · getGAAreas · holdGA ·
bestAvailable · release · releaseLabels · getSelection ·
selectObjects · deselectObjects · clearSelection · selectCategories ·
deselectCategories · setSelectableObjects · setMaxSelection ·
getSelectionValidity · setSeatTier · getFloors · setFloor ·
setColorblindSafe · zoomIn · zoomOut · zoomToFit · refreshAccess
Calling any of them before the chart exists returns an empty answer rather than throwing, so a ref used one frame early is safe.
| Prop | Payload |
|---|---|
onSelectionChange |
SelectedSeat[] |
onSelectionValidityChange |
Rule state with typed violations |
onSelectionValid / onSelectionInvalid |
Rule-validity transition |
onSelectionLimit |
Active numeric cap |
onHold |
HoldResult |
onHoldRestored |
HoldResult |
onHoldExpired |
no payload |
onGAClick |
GAAreaAvailability |
onError |
unknown |
onDeckTap |
string (floor id) |
onHint |
string | null: null clears the hint |
onSeatHover |
SeatHoverDetails | null: null when the pointer leaves |
onAccessExpired |
BuyerAccessExpiredEvent |
onAccessUnavailable |
BuyerAccessUnavailableEvent |
onSelectedObjectUnavailable |
SelectedObjectUnavailableEvent |
Install @seatlayer/react, render <SeatingChart event="ev_…" /> inside a
container with a definite height, and read the buyer's choice from
onSelectionChange. That is a complete interactive seating chart with live
availability; the quick start above is the whole integration, and the
seat-picker documentation
covers props, events, holds, and checkout in depth.
SeatingChart, SeatPicker, and SeatManager are real React components that
render a plain <div> into your own tree, with no iframe, no portal, and no
stylesheet of their own to fight with. EmbeddedDesigner is the one exception:
the organizer Designer is deliberately hosted in a sandboxed iframe so no
Designer credential ever reaches your bundle. If you want an iframe for the
buyer picker, attachPickerFrame is exported for that.
SeatingChart is the chart alone: it draws the venue, manages selection, and
hands you the seats, and you build the surrounding UI. SeatPicker is the complete
buyer experience with legend, priced tray, hold timer, and checkout hand-off
already built. Start with SeatPicker if you want a working ticket flow today,
and drop to SeatingChart when your design system owns the chrome.
When a buyer commits to a selection, hold() reserves that inventory against
concurrent buyers for a limited checkout window and returns an opaque holdId.
The hold lapses on its own if checkout never completes. onHoldExpired tells
the app to return the buyer to the map, and resumeHold() restores it after a
same-tab checkout navigation or a reload. This is what prevents double-selling
without locking seats forever.
Yes. Nothing in this package takes a payment. The browser hands you a holdId
and a self-contained CheckoutHandoff with the priced line items, your backend
charges through whatever provider you already use, and it then books the hold
through the
server-side checkout flow.
The opt-in checkout: 'hosted' mode exists for hosts with no backend at all;
without it, no payment code is downloaded.
You can explore a live seating chart in the browser at the buyer seat-map demo with no account. Rendering your own venue needs an event key, because the chart and its availability are served by the SeatLayer API: create a free test event for that, which books no real inventory.
Yes, on the client. The chart is built inside an effect and touches the DOM only
there, so a server render emits the empty container. This package ships no
'use client' banner of its own, so in the App Router mark your own component
that imports it, or load it through next/dynamic with ssr: false.
- Follow the buyer SDK installation guide for the full browser integration, options, and events.
- Build a React or Next.js seating chart with the framework-specific tutorial and client-mounting guidance.
- Connect seat holds to secure server-side checkout without putting booking credentials in the browser.
- Run the complete checkout example to connect a buyer hold id to payment and idempotent booking.
- Compare SeatLayer's mobile seat map SDKs when the same event also has to render in native iOS, Android, Flutter, or React Native apps.
- Read the embedded Designer guide to let organizers draw their own venues inside your product.
- Explore the 3D seating chart for the interactive venue view buyers can switch to from the map.
- Point AI coding agents at the SeatLayer docs index
(
llms.txt) for an agent-readable map of the documentation.
| Surface | Package or source |
|---|---|
| JavaScript | @seatlayer/js |
| React | @seatlayer/react (this package) |
| Vue | @seatlayer/vue |
| Angular | @seatlayer/angular |
| React Native | @seatlayer/react-native |
| iOS | seatlayer-ios |
| Flutter | seatlayer |
| Android | seatlayer-android |
| Server SDKs | Node.js, Python, PHP, Ruby, .NET, Java, and Go |
pnpm install
pnpm verifySource, issues, and contribution guidance live in seatlayer/seatlayer-sdk.
The React wrapper source in this package is MIT licensed. The separately distributed SeatLayer runtime is not covered by this package's MIT license.