Skip to content
Docs
Quickstart

Vite + React quickstart

A client-only React app (what Lovable and Bolt generate) with the Live SDK, React Elements wrappers and the hosted checkout modal.

A client-only React app: no server, no backend, no database. The publishable key is public, the cart lives in the Live session, and payment happens in the hosted checkout, so a static bundle is all you deploy.

Install#

npm create vite@latest my-tickets -- --template react-ts
cd my-tickets
npm install @ticketlayer/live @ticketlayer/elements @ticketlayer/elements-react
.env
VITE_LIVE_API_URL=https://live.ticketlayer.com
VITE_CHECKOUT_BASE=https://checkout.ticketlayer.com
VITE_TL_PUBLISHABLE_KEY=tlpk_...

:::note No build step? Use the script tag @ticketlayer/live, @ticketlayer/elements and @ticketlayer/elements-react are on npm, so the install below works. The section Without the npm packages is the same app built on the CDN loader instead, for a page with no bundler. :::

Boot once#

src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createLiveClient } from '@ticketlayer/live';
import { defineCustomElements } from '@ticketlayer/elements-react';
import App from './App';

defineCustomElements();

createLiveClient({
  baseUrl: import.meta.env.VITE_LIVE_API_URL,
  publishableKey: import.meta.env.VITE_TL_PUBLISHABLE_KEY,
  checkout: { mode: 'modal', embedBaseUrl: import.meta.env.VITE_CHECKOUT_BASE },
}).then((tl) => {
  tl.on('checkout:completed', (order: { id: string }) => {
    window.location.href = `/thanks?orderId=${encodeURIComponent(order.id)}`;
  });
});

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

Use the elements#

src/App.tsx
import { useState } from 'react';
import {
  TlEventList,
  TlBuyTicketsButton,
  TlCartBadge,
  TlCartDrawer,
  TlOrderConfirmation,
} from '@ticketlayer/elements-react';

export default function App() {
  const [eventId, setEventId] = useState<string | null>(null);
  const orderId = new URLSearchParams(window.location.search).get('orderId');

  if (orderId) return <TlOrderConfirmation />; // reads ?orderId= itself

  return (
    <div style={{ maxWidth: 960, margin: '2rem auto', fontFamily: 'system-ui' }}>
      <header style={{ display: 'flex', justifyContent: 'space-between' }}>
        <h1>What's on</h1>
        <TlCartBadge />
      </header>

      <TlEventList columns={3} onTlEventClick={(e: CustomEvent<{ eventId: string }>) => setEventId(e.detail.eventId)} />

      {eventId && (
        <section>
          <h2>Get tickets</h2>
          <TlBuyTicketsButton eventId={eventId} size="lg" />
        </section>
      )}

      <TlCartDrawer />
    </div>
  );
}

Every wrapper renders the matching custom element and turns each tl* event into an onTl* prop with the CustomEvent as its argument. Attributes are camelCased props (eventId, occurrenceId, fullWidth).

Without the npm packages#

Load the CDN build from index.html and write the tags directly. This is what works today and is also the smallest thing an AI tool can generate correctly.

index.html
<script src="https://cdn.ticketlayer.com/v1/ticketlayer.js"
        data-publishable-key="tlpk_..."
        data-api-url="https://live.ticketlayer.com"
        data-checkout-base="https://checkout.ticketlayer.com"
        data-elements-url="https://cdn.ticketlayer.com/elements/v1/ticketlayer-elements/ticketlayer-elements.esm.js"></script>
src/App.tsx
import { useEffect, useRef } from 'react';

export default function App() {
  const list = useRef<HTMLElement>(null);
  useEffect(() => {
    const el = list.current;
    const onClick = (e: Event) => console.log((e as CustomEvent<{ eventId: string }>).detail.eventId);
    el?.addEventListener('tlEventClick', onClick);
    return () => el?.removeEventListener('tlEventClick', onClick);
  }, []);
  return (
    <>
      <tl-cart-badge></tl-cart-badge>
      <tl-event-list ref={list} columns="3"></tl-event-list>
      <tl-buy-tickets-button event-id="evt_..."></tl-buy-tickets-button>
      <tl-cart-drawer></tl-cart-drawer>
    </>
  );
}

TypeScript needs to know the tags exist:

src/tl-elements.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from 'react';

type TlProps = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & Record<string, unknown>;

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'tl-event-list': TlProps;
      'tl-buy-tickets-button': TlProps;
      'tl-cart-badge': TlProps;
      'tl-cart-drawer': TlProps;
      'tl-order-confirmation': TlProps;
    }
  }
}

Attributes on raw tags are strings (columns="3"), and events are attached with addEventListener because React does not bind custom events on intrinsic elements. The checkout:completed hook is the same as in the HTML quickstart: window.ticketlayer.ready.then((tl) => tl.on('checkout:completed', ...)).

Next#