Tickets

List, filter, paginate and export a user's tickets with <u-ticketable-list>. Templates are plain HTML — the SDK stamps one copy per ticket and fills in <ticketable-value> placeholders, so you keep full control over markup and styling. The same list also powers peer-to-peer ticket transfers.

View this guide as Markdown

Ticket cards

A card layout driven by a <template> element. ticketable-value reads any field from the ticket — including nested metadata with fallback defaults — while ticketable-conditional and unidy-attr let the markup react to the data.

  • <template> rendering — one card stamped per ticket, your markup
  • Nested metadata — name="metadata.category" with default values
  • Conditional content — VIP badge and wallet export only when the data says so
  • Export — PDF download and Apple Wallet (pkpass)
<u-signed-in>
  <u-ticketable-list ticketable-type="ticket" limit="2" container-class="grid gap-4">
    <template>
      <div class="rounded-lg border border-border bg-white p-5 shadow-sm">
        <div class="flex items-start justify-between gap-4">
          <div>
            <h3 class="text-lg font-bold">
              <ticketable-value name="title"></ticketable-value>
            </h3>
            <p class="mt-1 text-sm text-text-light">
              <ticketable-value name="starts_at" date-format="dd.MM.yyyy HH:mm"></ticketable-value>
              –
              <ticketable-value name="ends_at" date-format="dd.MM.yyyy"></ticketable-value>
            </p>
            <p class="mt-2 font-semibold text-primary">
              <ticketable-value name="price" format="{{value}}" default="Free"></ticketable-value>
            </p>
            <!-- Nested metadata with a default value -->
            <p class="text-xs text-text-muted">
              Category: <ticketable-value name="metadata.category" default="General"
              ></ticketable-value>
            </p>
          </div>

          <!-- Conditional rendering based on ticket data -->
          <ticketable-conditional when="metadata.vip">
            <span class="rounded-full bg-warning-bg px-2 py-0.5 text-xs font-bold text-warning-text"
              >VIP</span
            >
          </ticketable-conditional>
        </div>

        <div class="mt-4 flex flex-wrap gap-2">
          <!-- Dynamic attributes from ticket data -->
          <a
            unidy-attr
            unidy-attr-href="{{button_cta_url}}"
            class="btn btn-primary !min-h-0 !px-3 !py-1.5 text-sm">
            View details
          </a>
          <u-ticketable-export
            format="pdf"
            class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-dark text-white hover:bg-dark-lighter transition-colors cursor-pointer disabled:opacity-50">
            Download PDF
          </u-ticketable-export>
          <!-- Wallet export only when the ticket supports it -->
          <ticketable-conditional when="exportable_to_wallet">
            <u-ticketable-export
              format="pkpass"
              class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-black text-white transition-colors cursor-pointer disabled:opacity-50">
              Add to Wallet
            </u-ticketable-export>
          </ticketable-conditional>
        </div>
      </div>
    </template>

    <!-- shown when the user has no tickets -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets yet.</p>
  </u-ticketable-list>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see your tickets.
  </p>
</u-signed-in>
Live demo

No tickets yet.

Sign in on the Auth page to see your tickets.

Table view with pagination and filtering

The target attribute renders rows into an existing element (here: a tbody), which makes tables easy. Pagination is provided by u-pagination-button/-page, and changing the filter attribute makes the list re-fetch — no custom data plumbing.

  • target rendering — rows go into your own table structure
  • Pagination — prev/next buttons and page indicator components
  • Live filtering — set the filter attribute; the SDK re-fetches
  • Skeleton loading — skeleton-all-text renders placeholders while loading
<u-signed-in>
  <div class="mb-3 flex items-center justify-end">
    <select id="state-filter" class="rounded-lg border border-border bg-white px-3 py-2 text-sm">
      <option value="">All states</option>
      <option value="active">Active</option>
      <option value="inactive">Inactive</option>
    </select>
  </div>

  <!-- target renders rows into the tbody; skeleton rows show while loading -->
  <u-ticketable-list
    id="tickets-list"
    ticketable-type="ticket"
    target="#tickets-table-body"
    limit="5"
    skeleton-all-text="true">
    <div class="overflow-x-auto">
      <table class="min-w-full rounded-lg border border-border bg-white">
        <thead
          class="bg-background-light text-left text-xs uppercase tracking-wider text-text-muted">
          <tr>
            <th class="px-4 py-3">Title</th>
            <th class="px-4 py-3">Starts</th>
            <th class="px-4 py-3">Price</th>
            <th class="px-4 py-3">Actions</th>
          </tr>
        </thead>
        <tbody id="tickets-table-body" class="divide-y divide-border"></tbody>
      </table>
    </div>

    <div class="mt-4 flex items-center gap-2">
      <u-pagination-button
        direction="prev"
        class-name="px-3 py-2 rounded-lg border border-border bg-white text-sm cursor-pointer hover:bg-background-light disabled:opacity-50 disabled:cursor-not-allowed">
      </u-pagination-button>
      <u-pagination-page class-name="px-3 py-2 text-sm"></u-pagination-page>
      <u-pagination-button
        direction="next"
        class-name="px-3 py-2 rounded-lg border border-border bg-white text-sm cursor-pointer hover:bg-background-light disabled:opacity-50 disabled:cursor-not-allowed">
      </u-pagination-button>
    </div>

    <!-- shown when the user has no tickets -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets yet.</p>

    <template>
      <tr class="transition-colors hover:bg-background-light">
        <td class="whitespace-nowrap px-4 py-3 font-medium">
          <ticketable-value name="title"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 text-sm text-text-light">
          <ticketable-value name="starts_at" date-format="yyyy-MM-dd HH:mm"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 font-semibold text-primary">
          <ticketable-value name="price" default="—"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3">
          <u-ticketable-export
            format="pdf"
            class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-dark text-white hover:bg-dark-lighter transition-colors cursor-pointer disabled:opacity-50">
            PDF
          </u-ticketable-export>
        </td>
      </tr>
    </template>
  </u-ticketable-list>

  <script is:inline>
    // Update the list's filter attribute; the SDK re-fetches automatically
    document.getElementById("state-filter").addEventListener("change", (event) => {
      const list = document.getElementById("tickets-list");
      list.setAttribute("filter", event.target.value ? `state=${event.target.value}` : "");
    });
  </script>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see the table view.
  </p>
</u-signed-in>
Live demo
Title Starts Price Actions

No tickets yet.

Sign in on the Auth page to see the table view.

Transfer a ticket

Drop a u-ticket-transfer-form into a ticket <template> and u-ticketable-list stamps each ticket's id onto it — no wiring. The user types a recipient email and the SDK creates a pending transfer offer.

  • u-ticket-transfer-form — email input + submit, styled via *-class-name
  • Auto ticket-id — the ticket list stamps the id when nested
  • Inline feedback — error-class-name / success-class-name outputs
  • uTicketTransferCreate* events — success/error events carry the created transfer
<u-signed-in>
  <!-- A u-ticket-transfer-form nested inside a ticket template needs no
       ticket-id: u-ticketable-list stamps each ticket's id onto it. -->
  <u-ticketable-list ticketable-type="ticket" limit="3" container-class="grid gap-4">
    <template>
      <div class="rounded-lg border border-border bg-white p-5 shadow-sm">
        <h3 class="text-lg font-bold">
          <ticketable-value name="title"></ticketable-value>
        </h3>
        <p class="mt-1 mb-4 text-sm text-text-light">
          <ticketable-value name="starts_at" date-format="dd.MM.yyyy HH:mm"></ticketable-value>
        </p>
        <u-ticket-transfer-form
          class-name="flex flex-wrap items-start gap-2"
          input-class-name="flex-1 min-w-48 rounded-lg border border-border bg-white px-3.5 py-2.5 text-base focus:border-primary focus:outline-none"
          button-class-name="btn btn-primary !min-h-0 !px-4 !py-2.5 text-sm"
          error-class-name="mt-1 w-full text-sm text-danger"
          success-class-name="mt-1 w-full text-sm text-success-text"></u-ticket-transfer-form>
      </div>
    </template>

    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets to transfer.</p>
  </u-ticketable-list>

  <p id="send-status" class="mt-4 text-sm text-text-light" role="status" aria-live="polite"></p>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to transfer a ticket.
  </p>
</u-signed-in>

<script>
  // The form shows its own inline success/error; the event also carries the
  // created transfer so the host app can act on it (e.g. refresh a list).
  const status = document.getElementById("send-status");
  const show = (msg) => status && (status.textContent = msg);

  document.addEventListener("uTicketTransferCreateSuccess", (e) =>
    show(`Offer sent to ${e.detail.transfer.recipient_email}`)
  );
  document.addEventListener("uTicketTransferCreateError", (e) =>
    show(`Transfer failed: ${e.detail.error}`)
  );
</script>
Live demo

No tickets to transfer.

Sign in on the Auth page to transfer a ticket.

Incoming & outgoing transfers

u-ticket-transfer-list renders pending offers per direction with its own <transfer-value> template — and <transfer-conditional> to show a block only when a field is present. u-ticket-transfer-action stamps the transfer token automatically, so accept/decline/cancel buttons need no data plumbing, and the bubbling uTicketTransferAction/List events let the host app react.

  • direction="incoming|outgoing" — one component, two views
  • <transfer-value> / <transfer-conditional> — fields + presence-gated blocks
  • accept / decline / cancel — u-ticket-transfer-action with auto token
  • uTicketTransferAction/List events — bubbling success/error events to react to
<u-signed-in>
  <div class="grid gap-8">
    <div>
      <h3 class="mb-3 text-sm font-semibold text-text-muted uppercase">Incoming</h3>
      <u-ticket-transfer-list direction="incoming" container-class="grid gap-3" skeleton-count="2">
        <template>
          <div
            class="flex flex-wrap items-start justify-between gap-3 rounded-lg border border-border bg-white p-4 shadow-sm">
            <div>
              <p class="font-bold">
                <transfer-value name="ticket.title"></transfer-value>
              </p>
              <p class="mt-1 text-sm text-text-light">
                From <transfer-value name="sender_email"></transfer-value> · expires
                <transfer-value name="expires_at" date-format="dd.MM.yyyy HH:mm"></transfer-value>
              </p>
              <!-- transfer-conditional shows a block only when the field is present -->
              <transfer-conditional when="ticket.price">
                <p class="mt-1 text-sm font-semibold text-primary">
                  <transfer-value name="ticket.price"></transfer-value>
                </p>
              </transfer-conditional>
            </div>
            <div class="flex gap-2">
              <u-ticket-transfer-action
                action="accept"
                class-name="btn btn-primary !min-h-0 !px-3 !py-1.5 text-sm">
                Accept
              </u-ticket-transfer-action>
              <u-ticket-transfer-action
                action="decline"
                class-name="btn btn-outline !min-h-0 !px-3 !py-1.5 text-sm">
                Decline
              </u-ticket-transfer-action>
            </div>
          </div>
        </template>
        <p slot="empty" class="text-sm text-text-light">No incoming transfers.</p>
      </u-ticket-transfer-list>
    </div>

    <div>
      <h3 class="mb-3 text-sm font-semibold text-text-muted uppercase">Outgoing</h3>
      <u-ticket-transfer-list direction="outgoing" container-class="grid gap-3" skeleton-count="2">
        <template>
          <div
            class="flex flex-wrap items-start justify-between gap-3 rounded-lg border border-border bg-white p-4 shadow-sm">
            <div>
              <p class="font-bold">
                <transfer-value name="ticket.title"></transfer-value>
              </p>
              <p class="mt-1 text-sm text-text-light">
                To <transfer-value name="recipient_email"></transfer-value> · expires
                <transfer-value name="expires_at" date-format="dd.MM.yyyy HH:mm"></transfer-value>
              </p>
            </div>
            <u-ticket-transfer-action
              action="cancel"
              class-name="btn btn-outline !min-h-0 !px-3 !py-1.5 text-sm">
              Cancel
            </u-ticket-transfer-action>
          </div>
        </template>
        <p slot="empty" class="text-sm text-text-light">No outgoing transfers.</p>
      </u-ticket-transfer-list>
    </div>

    <p id="manage-status" class="text-sm text-text-light" role="status" aria-live="polite"></p>
  </div>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to manage transfers.
  </p>
</u-signed-in>

<script>
  // The transfer components bubble events so the host app can react (toasts,
  // analytics, refetching). The lists already refetch themselves on success.
  const status = document.getElementById("manage-status");
  const show = (msg) => status && (status.textContent = msg);

  document.addEventListener("uTicketTransferActionSuccess", (e) =>
    show(`${e.detail.action} succeeded`)
  );
  document.addEventListener("uTicketTransferActionError", (e) =>
    show(`${e.detail.action} failed: ${e.detail.error}`)
  );
  document.addEventListener("uTicketTransferListError", (e) =>
    show(`Could not load transfers: ${e.detail.error}`)
  );
</script>
Live demo

Incoming

No incoming transfers.

Outgoing

No outgoing transfers.

Sign in on the Auth page to manage transfers.

Revoke a sent transfer

Once a transfer is accepted the ticket moves to the recipient (holder_id is set). action="revoke" pulls it back — it is ticket-id based, so u-ticketable-list auto-stamps the id. While a ticket is held by someone else the list also sets exportable=false, so u-ticketable-export is blocked until the ticket returns.

  • action="revoke" — ticket-id based, auto-stamped by the list
  • Export guard — exportable=false while the ticket is held away
  • ticket payload — uTicketTransferActionSuccess returns the updated ticket
<u-signed-in>
  <!-- action="revoke" needs a ticket-id, not a token; u-ticketable-list stamps
       it from each ticket. Revoke only succeeds while a ticket is transferred
       away (holder_id set) — otherwise the action emits ticket_not_transferred. -->
  <u-ticketable-list ticketable-type="ticket" limit="5" container-class="grid gap-4">
    <template>
      <div class="rounded-lg border border-border bg-white p-5 shadow-sm">
        <h3 class="text-lg font-bold">
          <ticketable-value name="title"></ticketable-value>
        </h3>
        <p class="mt-1 text-sm text-text-light">
          <ticketable-value name="starts_at" date-format="dd.MM.yyyy HH:mm"></ticketable-value>
        </p>
        <div class="mt-4 flex flex-wrap items-center gap-2">
          <!-- The list sets exportable=false while the ticket is held by
               someone else, so export is blocked until it's back. -->
          <u-ticketable-export
            format="pdf"
            class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-dark text-white hover:bg-dark-lighter transition-colors cursor-pointer disabled:opacity-50">
            Download PDF
          </u-ticketable-export>
          <u-ticket-transfer-action
            action="revoke"
            class-name="btn btn-outline !min-h-0 !px-3 !py-1.5 text-sm">
            Revoke transfer
          </u-ticket-transfer-action>
        </div>
      </div>
    </template>

    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets.</p>
  </u-ticketable-list>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to manage your tickets.
  </p>
</u-signed-in>
Live demo

No tickets.

Sign in on the Auth page to manage your tickets.

Return a held ticket

The mirror of revoke for the holder: action="return" gives a ticket you were transferred back to its owner. Also ticket-id based and auto-stamped, so a plain button in a ticket template is all it takes.

  • action="return" — holder gives the ticket back
  • Same list, different action — u-ticketable-list ticket template
<u-signed-in>
  <!-- action="return" gives a ticket back to its owner. Like revoke it is
       ticket-id based (auto-stamped) and applies to tickets held by you. -->
  <u-ticketable-list ticketable-type="ticket" limit="5" container-class="grid gap-4">
    <template>
      <div
        class="flex flex-wrap items-start justify-between gap-3 rounded-lg border border-border bg-white p-5 shadow-sm">
        <div>
          <h3 class="text-lg font-bold">
            <ticketable-value name="title"></ticketable-value>
          </h3>
          <p class="mt-1 text-sm text-text-light">
            <ticketable-value name="starts_at" date-format="dd.MM.yyyy HH:mm"></ticketable-value>
          </p>
        </div>
        <u-ticket-transfer-action
          action="return"
          class-name="btn btn-outline !min-h-0 !px-3 !py-1.5 text-sm">
          Return to owner
        </u-ticket-transfer-action>
      </div>
    </template>

    <p slot="empty" class="py-6 text-center text-sm text-text-light">No held tickets.</p>
  </u-ticketable-list>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see tickets held by
    you.
  </p>
</u-signed-in>
Live demo

No held tickets.

Sign in on the Auth page to see tickets held by you.