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 MarkdownTicket 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> 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> | 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> 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> Incoming
From
No incoming transfers.
Outgoing
To
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> 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>