This feature is currently in beta. Documentation for Version 1 templates is here

Template Markup & Structure — How to build and customise the HTML template for your waiting room.

See also: Vue State & Methods | Styling & CSS Variables

Getting Started

Open the template HTML file in both a text editor and a browser. The template contains a <script id="crowdhandler-data" type="application/json"> tag with a JSON object that configures the waiting room for testing.

<script id="crowdhandler-data" type="application/json">
{
    "dev": true,
    "status": 1,
    "title": "Test Event",
    "position": 150,
    "estimate": 12,
    "simulatePolling": true
}
</script>

With "dev": true set, the template uses the values in this JSON object instead of making real API calls. Change the properties, save the file, and refresh the browser to see different states.

Preview Status Values

StatusState
1Active Queue
3Blocked
4Countdown
5Room Full

For the full list of preview properties, see the Preview Mode reference.

Preview Examples

// Active queue
{"dev": true, "status": 1, "position": 100, "estimate": 10}

// Countdown
{"dev": true, "status": 4, "onsale": "2026-02-15T14:00:00Z"}

// Blocked
{"dev": true, "status": 3}

Overview

The template uses Vue 3 directives to conditionally render sections based on the queue state. The main state object ch contains all queue data and is automatically updated via API polling (default every 60 seconds, variable TTL).

Required Elements

<!-- Container that shows when loaded -->
<div id="crowdhandler-template" :class="{ 'active': ch.isLoaded }" role="main">
  <!-- Screen reader announcements -->
  <div class="ch-sr-only" aria-live="polite" aria-atomic="true">{{ ch.announcement }}</div>

  <!-- Your template content -->

  <!-- Captcha container (required if captcha is enabled) -->
  <div id="ch-captcha-form"></div>

  <!-- Script inclusion -->
  <script type="module" src="/src/main.js"></script>
</div>

Queue States

The default template uses ch.captchaRequired as the top-level gate, then checks other states within the non-captcha branch using a v-if / v-else-if chain.

1. Captcha Required

ch.captchaRequired

User must complete captcha before entering queue. The captcha widget renders automatically in #ch-captcha-form.

<div v-if="ch.captchaRequired" class="ch-captcha-section">
  <p class="ch-captcha-message">Please verify you're human to continue</p>
  <div id="ch-captcha-form"></div>
</div>
<div v-else>
  <!-- All other states go here -->
</div>

To test captcha add the required captcha properties in the

<script id="crowdhandler-data" type="application/json"> tag. This will display the captcha block but will not allow live testing of captcha due to browser security.

<script id="crowdhandler-data" type="application/json">
{
    "dev": true,
    "status": 1,
    "title": "Test Event",
    "captchaRequired":true,
    "captchaType":"recaptcha",
    "captchaKeyPublic":"1234567890"
}
</script>

2. Blocked

ch.isBlocked

User has been blocked due to suspicious activity.

<div v-if="ch.isBlocked" class="ch-state-message ch-state-danger" role="alert">
  <h3 class="ch-state-title">Access Denied</h3>
  <p class="ch-state-description">You've been blocked due to suspicious activity.</p>
</div>

3. Room Full

ch.isRoomFull

Queue is at maximum capacity. Must use v-else-if after blocked check.

<div v-else-if="ch.isRoomFull" class="ch-state-message ch-state-warning" role="alert">
  <h3 class="ch-state-title">Queue Full</h3>
  <p class="ch-state-description">We're at capacity. You'll be let in when a spot opens up.</p>
</div>

4. Countdown (Pre-Queue)

ch.isCountdown

The queue hasn't opened yet. Show a countdown. sHours, sMinutes and sSeconds are zero-padded strings.

<div v-if="ch.isCountdown" class="ch-countdown">
  <p class="ch-countdown-label">Queue opens in</p>
  <div class="ch-countdown-display">
    <div class="ch-countdown-unit">
      <div class="ch-countdown-value">{{ ch.countdown.sHours }}</div>
      <div class="ch-countdown-suffix">hrs</div>
    </div>
    <div class="ch-countdown-unit">
      <div class="ch-countdown-value">{{ ch.countdown.sMinutes }}</div>
      <div class="ch-countdown-suffix">min</div>
    </div>
    <div class="ch-countdown-unit">
      <div class="ch-countdown-value">{{ ch.countdown.sSeconds }}</div>
      <div class="ch-countdown-suffix">sec</div>
    </div>
  </div>
</div>

5. Active Queue

ch.isActive

User is in the active queue with a position. When the user reaches the front, they are automatically redirected.

<div v-if="ch.isActive">
  <h2 v-if="ch.position">{{ ch.position }}</h2>
  <div v-else>Checking in!</div>
</div>

Template Sections

Header (Logo + Title)

Title gets a dynamic size class based on character count.

<header class="ch-header">
  <div class="ch-logo ch-enter ch-stagger-1" v-if="ch.logo">
    <img :src="ch.logo" :alt="ch.title" />
  </div>
  <div class="ch-header-row ch-enter ch-stagger-4">
    <h1 class="ch-title" :class="ch.css.title">{{ ch.title }}</h1>
  </div>
</header>

Message

Displayed above the card. Supports newlines via white-space: pre-line. Announced to screen readers when it changes.

<div class="ch-message ch-message-box ch-enter ch-stagger-6" v-if="ch.message">
  {{ ch.message }}
</div>

Countdown + Active Queue Wrapper

These states share a wrapper since the queue transitions from countdown to active.

<div v-if="ch.isCountdown || ch.isActive" class="ch-queue-states ch-enter ch-stagger-4">
  <!-- Countdown, position, progress bar, priority code, stock -->
</div>

Position Display

<div class="ch-position-display" v-if="ch.position && ch.isActive">
  <div class="ch-position-label">Your position</div>
  <h2 class="ch-position-number" :key="ch.position"><strong>{{ ch.position }}</strong></h2>
</div>

Progress Bar

Shows queue progress when a position exists. Shows a poll-based progress bar when checking in (ch.position is null).

<!-- With position -->
<div v-if="ch.position && ch.isActive" class="ch-progress-container">
  <div class="ch-progress-bar" role="progressbar"
    :aria-valuenow="Math.round(ch.progress || 0)"
    aria-valuemin="0" aria-valuemax="100">
    <div class="ch-progress-fill"
      :style="{ width: (isNaN(ch.progress) ? 10 : ch.progress) + '%' }"></div>
  </div>
  <div class="ch-progress-info">
    <span v-if="ch.estimate && ch.estimate <= 1">Almost there!</span>
    <span v-else-if="ch.estimate && ch.estimate > 60">More than an hour</span>
    <span v-else-if="ch.estimate">Approximately {{ ch.estimate }} minutes</span>
    <span v-if="ch.eta" class="ch-eta">ETA: {{ ch.eta }}</span>
  </div>
</div>

<!-- Checking in (no position yet) -->
<div v-else-if="ch.isActive" class="ch-progress-container">
  <div class="ch-position-label ch-checking-in">Checking in!</div>
  <div class="ch-progress-bar" role="progressbar"
    :aria-valuenow="Math.round(ch.nextPoll || 0)"
    aria-valuemin="0" :aria-valuemax="ch.pollTTL">
    <div class="ch-progress-fill"
      :style="{ width: (isNaN(ch.nextPoll) ? 0 : (ch.nextPoll / ch.pollTTL * 100)) + '%' }">
    </div>
  </div>
</div>

Stock Indicator

<div v-if="ch.stock !== null" class="ch-stock-badge"
  :class="{
    'ch-stock-low': ch.stock > 0 && ch.stock <= 50,
    'ch-stock-zero': ch.stock === 0
  }">
  <span v-if="ch.stock > 0">
    <span class="ch-stock-count">{{ ch.stock }}</span> items remaining
  </span>
  <span v-else>Sold out</span>
</div>

Session Info

<!-- During countdown -->
<div v-if="ch.isCountdown" class="ch-session-info">
  Stay on this page. You'll get a random position when the queue opens.
</div>

<!-- During active queue -->
<div v-if="ch.isActive">
  <span v-if="ch.sessionsExpire" class="ch-session-info">
    Keep this window open. You'll move forward automatically.
  </span>
  <span v-else-if="ch.sessionsTimeout" class="ch-session-info">
    You'll be redirected when it's your turn.
    You have {{ ch.sessionsTimeout }} minutes to complete your transaction.
  </span>
</div>

Email Notification Form

Three states: input form, submitting spinner, and confirmation. Users can change their email after submission. Only shown during active queue.

<div v-if="ch.emailAvailable && ch.isActive" class="ch-form-section ch-email-section">
  <!-- Submitting -->
  <div v-if="ch.email.submitting" class="ch-email-submitting">
    <div class="ch-spinner" role="status" aria-label="Submitting"></div>
  </div>

  <!-- Form -->
  <div v-else-if="!ch.email.submitted || ch.email.changing">
    <div v-if="ch.email.changing" class="ch-form-title">Change notification email</div>
    <div v-else class="ch-form-title">Get notified when it's your turn</div>
    <div class="ch-input-group">
      <input type="email" class="ch-input" placeholder="your@email.com"
        v-model="ch.email.value" @keyup="validateEmail" />
      <button class="ch-btn" @click="submitEmail" :disabled="!ch.email.valid">Notify me</button>
      <button v-if="ch.email.changing" class="ch-btn ch-btn-secondary"
        @click="cancelChangeEmail">Cancel</button>
    </div>
  </div>

  <!-- Confirmation -->
  <div v-else role="status">
    <div class="ch-form-feedback ch-success">We'll email {{ ch.email.value }}</div>
    <button @click="changeEmail" class="ch-btn ch-btn-sm ch-btn-change">Change</button>
  </div>
</div>

Priority Code Form

Available during both countdown and active queue states.

<div v-if="ch.priority && ch.priority.available" class="ch-form-section ch-priority-section">
  <div class="ch-form-title">Got a priority code?</div>
  <div class="ch-input-group">
    <input type="text" class="ch-input" id="ch-priority-input"
      placeholder="Enter code" v-model="ch.priority.code"
      @keyup="checkValidPriorityCode" />
    <button class="ch-btn" @click="prioritySubmit"
      :disabled="ch.priority.submitting || !ch.priority.code">Apply</button>
  </div>
  <div v-if="ch.priority.submitted && ch.priority.success"
    class="ch-form-feedback ch-success" role="status">Code applied!</div>
  <div v-if="ch.priority.submitted && ch.priority.error"
    class="ch-form-feedback ch-error" role="status">Invalid code</div>
</div>

Footer

Three-column layout: last check-in time (left), session token (center), powered-by (right).

<div class="ch-footer ch-enter ch-stagger-5" v-if="!ch.captchaRequired">
  <div class="ch-footer-left">
    <div class="ch-footer-meta" v-if="ch.requested">
      <span>Last check in:</span>
      <code class="ch-footer-meta-value">
        <span :class="{ 'ch-text-shimmer': ch.nextPoll < 3 }">
          {{ formatDate(ch.requested, 'toLocaleTimeString') }}
        </span>
      </code>
    </div>
  </div>
  <div class="ch-footer-center">
    <div class="ch-token-display" v-if="ch.token.obfuscated || ch.token.value">
      <span>ID:</span>
      <code class="ch-token-value" tabindex="0" role="button"
        @click="(e) => ch.token.copy(e, ch)">
        {{ ch.token.copied ? 'Copied!' : (ch.token.obfuscated || ch.token.value) }}
      </code>
    </div>
  </div>
  <div class="ch-powered-by">
    Powered by <a href="https://www.crowdhandler.com/..." target="_blank">CrowdHandler</a>
  </div>
</div>

Customisation Philosophy

The index.html template is a reference implementation — it covers every feature the system supports.

Keep the features, change the design

Every section is optional, except the #crowdhandler-template container and the script include, but we strongly recommend keeping all of the functionality in your template. The application won't break if you remove a feature's markup — it simply won't render UI for it. However, it's much easier to have the markup already in place when you need to enable a feature during a live on-sale. For example, if you decide to turn on captcha or stock display mid-event, the template renders it with no further changes. Without the markup, you'd need to update and re-deploy the template — and template caching means that update won't take effect straight away.

Reshape the state logic

The Vue directives are yours to rearrange. You could check ch.stock === 0 first and show a full-screen "Sold Out" panel, combine countdown with a teaser message, or embed the priority code form inside the countdown. The reference template groups states in a particular hierarchy, but you're free to restructure.

Build around the data, not the markup

Think of ch as a data API rather than a prescribed layout. The state properties are reactive values you can use anywhere, in any combination, with whatever conditional logic makes sense for your design. The reference template is a starting point, not a constraint.