Escape

Event ticket template for JavaScript

A ticket has two readers. The person holding it wants the date, the venue, and their seat. The scanner at the door wants a code it can check against the guest list. An order for three people is three tickets. Each one has its own name and its own code, so a forwarded email can’t let three people in on one ticket. This is a ready-made quario event ticket — copy it, point it at your orders, and render one ticket per attendee to PDF or HTML.

The template

The whole ticket is a definition — data you can version and review. The event block, the QR code, the date and venue grid, the seat line and the small print are all in here. This is the exact file the preview below renders:

event-ticket.report.json
{
  "data": "$.order.tickets[*]",
  "sort": [{ "by": "[email protected]" }],
  "aggregates": { "tickets": "count" },
  "style": { "family": "sans", "size": 9 },
  "groups": [{ "name": "ticket", "by": "[email protected]", "break": "between" }],
  "detail": [
    {
      "type": "split",
      "slots": [
        {
          "type": "text",
          "width": 80,
          "value": [
            { "value": "{{ $.input.order.event.presenter }}\n", "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97" } },
            { "value": "{{ $.input.order.event.name }}\n", "style": { "family": "serif", "size": 24, "bold": true, "color": "#1e1f22" } },
            { "value": "{{ $.input.order.event.subtitle }}", "style": { "size": 9, "color": "#5e6062" } }
          ],
          "style": { "paddingRight": 18 }
        },
        { "type": "image", "source": "=qr(@.code)", "fit": "width", "alt": "Entry code {{ @.code }}" }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 38 }, { "width": 38 }, {}],
      "rows": [
        {
          "style": { "family": "mono", "size": 6, "uppercase": true, "color": "#989a97", "paddingTop": 16 },
          "slots": [{ "type": "text", "value": "Date" }, { "type": "text", "value": "Venue" }, { "type": "text", "value": "Entry code" }]
        },
        {
          "style": { "size": 10, "color": "#1e1f22" },
          "slots": [
            { "type": "text", "value": "{{ $.input.order.event.date }}", "style": { "bold": true, "format": { "kind": "date", "form": "full" } } },
            { "type": "text", "value": "{{ $.input.order.event.venue }}", "style": { "bold": true } },
            { "type": "text", "value": "{{ @.code }}", "style": { "family": "mono", "size": 8 } }
          ]
        },
        {
          "style": { "size": 9, "color": "#5e6062", "paddingBottom": 8 },
          "slots": [
            { "type": "text", "value": "Doors {{ $.input.order.event.doors }}, starts {{ $.input.order.event.starts }}" },
            { "type": "text", "value": "{{ $.input.order.event.address }}" },
            { "type": "text", "value": "" }
          ]
        }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 38 }, { "width": 12 }, { "width": 12 }, { "width": 14 }, { "style": { "align": "right" } }],
      "rows": [
        {
          "style": { "family": "mono", "size": 6, "uppercase": true, "color": "#989a97", "paddingTop": 8,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "slots": [
            { "type": "text", "value": "Ticket holder" },
            { "type": "text", "value": "Section" },
            { "type": "text", "value": "Row" },
            { "type": "text", "value": "Seat" },
            { "type": "text", "value": "{{ @.type }}" }
          ]
        },
        {
          "style": { "size": 13, "bold": true, "color": "#1e1f22" },
          "slots": [
            { "type": "text", "value": "{{ @.holder }}" },
            { "type": "text", "value": "{{ @.section }}" },
            { "type": "text", "value": "{{ @.row }}" },
            { "type": "text", "value": "{{ @.seat }}" },
            { "type": "text", "value": "{{ @.price }}", "style": { "format": "currency" } }
          ]
        }
      ]
    },
    { "type": "text", "value": "Ticket {{ @.number }} of {{ $.tickets }} · Order {{ $.input.order.number }} · Admits one. Scan the code at the door, on paper or on screen.",
      "style": { "size": 7, "color": "#989a97", "spaceBefore": 16, "spaceAfter": 60 } }
  ]
}

The rows are the tickets, $.order.tickets[*], and the event is $.input. One ticket per page is a group on the ticket number with "break": "between". The QR code is an image item in the second slot of the first split, and its source is =qr(@.code). That ticket’s entry code goes in, and the code comes back as SVG bytes the slot scales to its width.

The event block is one text slot written as three styled runs. The presenter line is small mono capitals, the event name is the serif at 24 point, and the subtitle is plain. It’s one cell in three faces, with the line breaks inside the runs.

The date declares { "kind": "date", "form": "full" }, so 2026-09-12 reads Saturday 12 September 2026 under en-IE. The price is a bare number under format: "currency", and the label above it reads the ticket’s own type from the row.

The QR code

bwip-js draws the QR code as SVG, synchronously, and it runs in Node and in the browser. A library that needs await would have to draw before the render and pass the bytes in, because an image source can’t wait:

qr.ts
import bwipjs from "bwip-js";

/** A QR code of `text`, returned as the SVG's bytes. */
export function qr(text: string): Uint8Array {
  const svg = bwipjs.toSVG({ bcid: "qrcode", text });
  // bwip-js sizes the drawing by `viewBox` alone and leaves the modules' fill to the default,
  // which not every SVG renderer reads as black; the image item needs both stated.
  const [, , width, height] = svg.match(/viewBox="([^"]+)"/)![1].split(" ");
  return new TextEncoder().encode(
    svg
      .replace("<svg ", `<svg width="${width}" height="${height}" `)
      .replace("<path ", '<path fill="#000000" '),
  );
}

The code holds the ticket’s entry code alone, not a URL with the holder’s name in it. Your door app looks the code up, so you can cancel and reissue a lost ticket without reprinting the rest of the order.

The data

A definition renders nothing on its own — you hand it a document. This is the sample the preview below uses. Each ticket carries its holder, its seat, its price, and its code. The event sits once on the order:

event-ticket.data.json
{
  "order": {
    "number": "ORD-26-0912",
    "event": {
      "presenter": "Quire Press presents",
      "name": "Letterpress Night",
      "subtitle": "An evening on the Heidelberg platen, with type to set and a print to take home",
      "date": "2026-09-12",
      "doors": "19:00",
      "starts": "19:30",
      "venue": "Quire Press studio",
      "address": "Binnenkant 42, 1011 BM Amsterdam"
    },
    "tickets": [
      { "number": 1, "code": "QP-0912-4471-A", "holder": "Aoife Byrne", "type": "Standard", "section": "Studio", "row": "B", "seat": "7", "price": 18 },
      { "number": 2, "code": "QP-0912-4471-B", "holder": "Ciarán Byrne", "type": "Standard", "section": "Studio", "row": "B", "seat": "8", "price": 18 },
      { "number": 3, "code": "QP-0912-4471-C", "holder": "Niamh Byrne", "type": "Concession", "section": "Studio", "row": "B", "seat": "9", "price": 12.5 }
    ]
  }
}

What it renders

This isn’t a screenshot. quario compiles the definition and data above, runs qr.ts in your browser for each ticket, and @quario/viewer paints three tickets right here. The PDF button hands you the same tickets as real @quario/pdf bytes, one 200 × 100 mm page each, with the QR modules as vector paths. There’s no Word button: @quario/docx refuses an SVG image.

Rendering live with @quario/viewer200 × 100 mm ticket

Use it

shell
npm install quario @quario/pdf bwip-js

The ticket’s size is a render option, not part of the document. size takes a [width, height] in points, and 200 × 100 mm is 566.93 × 283.46:

event-ticket.ts
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import ticket from './event-ticket.report.json' with { type: 'json' }

// `qr` is the function from qr.ts
const q = quario({ locale: 'en-IE', currency: 'EUR' })   // what `format` presents through
const report = q.report(ticket, { qr })

const data = await getOrder(id)                      // your ticketing data
const bytes = await report.render(pdf({ page: { size: [566.93, 283.46], margin: 24 } }), data)

writeFileSync('tickets.pdf', bytes)                  // one page per ticket

Attach the PDF to the confirmation email, or render each ticket on its own for a wallet pass page. Either way it’s the same definition.

Make it yours

Because the ticket is declarative, changing it means changing data. A ticket on A4 for printing at home is a different size in the render call. A general-admission event drops the section, row and seat columns. A festival pass with several days is the dates as a list in the data. A barcode in place of the QR code is a different bcid in the function, as on the shipping label. It renders without eval, so a name typed at checkout never becomes code. Security →

FAQ

Will a phone scan the QR code from a screen?

Yes. The code is vector paths in the PDF, so it stays sharp at any zoom. Keep it at least 2 cm across on paper, and leave the white margin around it, which the scanner needs to find the code.

Can I generate the tickets as a PDF?

Yes, and it’s the format a confirmation email carries. @quario/pdf renders this definition straight to bytes with no browser anywhere, so your checkout can issue tickets the moment a customer pays. Full example →

Can I use this event ticket template commercially?

The definition is yours — the report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Selling real tickets is past evaluation, so that needs a license for each production deployment. Its users, agents, and renders are never counted. Pricing →

Where are the other templates?

Browse the template gallery → — receipts, certificates, shipping labels, and more.