Skip to main content

Custom HTML Templates

Complete guide to creating custom HTML document templates for Payaca users. Covers Shadow DOM requirements, LiquidJS syntax, image handling, responsive design, and all available variables.

Matt Franklin avatar
Written by Matt Franklin
Updated this week

Custom HTML Templates

Overview

Payaca enables you to create professional, branded documents using HTML templates with LiquidJS templating syntax. These templates automatically pull data from your projects, customers, and custom fields to generate dynamic documents like commissioning certificates, installation reports, and compliance documentation.

Benefits over static PDFs:

  • Responsive design - Perfect display on any device (mobile, tablet, desktop) and prints beautifully

  • No manual data entry - Data flows automatically from Payaca projects

  • Easy updates - Change styling or layout without recreating forms

  • Better images - Full-resolution photos, image galleries, clickable to view full-size

  • Complete branding - Full control over colors, fonts, layout, and your company logo

  • Interactive features - Links, expandable sections, dynamic content

Common use cases:

  • Commissioning certificates (heat pumps, solar PV, etc.)

  • Installation reports and handover packs

  • Technical survey documents

  • Customer proposals and quotations

  • Compliance and warranty documentation

  • Service reports and maintenance records


Getting started

Your first template

Create a new file called my-template.html with this minimal structure:

<style>
  :host {
    display: block;
    --primary-color: #2563eb;
  }  :host * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }  .document-container {
    container-type: inline-size;
    container-name: document;
    font-family: Arial, sans-serif;
    padding: 2rem;
  }
</style><div class="document-container">
  <img src="{​{ account.companyLogo }​}" alt="Company Logo" style="height: 60px; margin-bottom: 1rem;">  <h1>Installation Report - {​{ project.reference }​}</h1>  <p><strong>Customer:</strong> {​{ customer.name }​}</p>
  <p><strong>Site Address:</strong> {​{ project.siteAddress }​}</p>
  <p><strong>Date:</strong> {​{ 'now' | date: "%d %B %Y" }​}</p>  <!-- Add your content sections here -->
</div>

Deploying your template

  1. Go to Payaca: Settings → Templates → Document Templates

  2. Click "Create document template"

  3. Upload your .html file (drag & drop or browse)

  4. Preview in Desktop and Mobile views

  5. Give it a descriptive name (e.g., "Heat Pump Commissioning Certificate")

  6. Click "Create template"

Using your template

  1. Open any project in Payaca

  2. Go to the Files section

  3. Click "Generate Document"

  4. Select your template from the dropdown

  5. Preview the document with real project data

  6. Click "Generate" to create the document

Important: When updating templates, download the current version first as Payaca doesn't keep version history!


Template structure

Shadow DOM compatibility (critical requirement)

Payaca templates run in a Shadow DOM environment. This means you MUST follow specific CSS rules or your template won't display correctly.

Required CSS patterns:

<style>
  /* Use :host instead of :root for CSS variables */
  :host {
    display: block;
    --primary-color: #2563eb;
    --secondary-color: #64748b;
    --border-color: #e2e8f0;
  }  /* Use :host * instead of * for universal selector */
  :host * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }  /* Use .document-container instead of body */
  .document-container {
    container-type: inline-size;
    container-name: document;
    font-family: Arial, sans-serif;
    color: #1e293b;
    padding: 2rem;
  }
</style>

Forbidden elements (will not work):

  • <html>, <head>, <body> tags

  • External CSS files (<link rel="stylesheet">)

  • All JavaScript (including <script> tags, inline JavaScript, event handlers like onclick, onload, etc.)

  • :root selector in CSS (use :host instead)

  • References to document.body in CSS

What works:

  • Inline <style> tags with Shadow DOM compatible CSS

  • All standard HTML5 tags (div, p, h1-h6, table, img, etc.)

  • CSS variables via :host

  • Embedded images (base64 encoded or external URLs)

  • Container queries for responsive design

  • Print-specific styles using @media print

Correct structure template:

<style>
  :host {
    display: block;
    --primary-color: #2563eb;
  }  :host * {
    margin: 0;
    padding: 0;
  }  .document-container {
    container-type: inline-size;
    container-name: document;
  }  /* Your custom styles here */
</style><div class="document-container">
  <!-- Your content here -->
</div>


Variables and data

Templates use LiquidJS syntax with double curly braces to insert dynamic data. Learn more at LiquidJS documentation.

Account (your company) variables:

{​{ account.companyName }​}                    <!-- Your company name -->
{​{ account.companyLogo }​}                    <!-- Your company logo URL -->
{​{ account.address }​}                        <!-- Auto-formatted full address -->
{​{ account.email }​}                          <!-- Company email -->
{​{ account.phone }​}                          <!-- Company phone -->
{​{ account.companyRegistrationNumber }​}      <!-- Registration number -->

Address component access:

{​{ account.address['line1'] }​}               <!-- "123 High Street" -->
{​{ account.address['line2'] }​}               <!-- "Unit 4" -->
{​{ account.address.city }​}                   <!-- "London" -->
{​{ account.address.postalCode }​}             <!-- "SW1A 1AA" -->
{​{ account.address.country }​}                <!-- "United Kingdom" -->

Project variables:

{​{ project.reference }​}                      <!-- Project reference number -->
{​{ project.siteAddress }​}                    <!-- Auto-formatted site address -->
{​{ project.siteAddress['line1'] }​}           <!-- Address line 1 -->
{​{ project.siteAddress.city }​}               <!-- City -->
{​{ project.siteAddress.postalCode }​}         <!-- Postcode -->

Customer variables:

{​{ customer.name }​}                          <!-- Customer name -->
{​{ customer.primaryContact.name }​}           <!-- Primary contact full name -->
{​{ customer.primaryContact.firstName }​}      <!-- First name only -->

Custom fields:

Access your custom fields using this syntax:

{​{ project.customFields.fieldsetId.fieldId }​}

Real examples:

<!-- Property information -->
{​{ project.customFields.propertyCharacteristics.yearBuilt }​}
{​{ project.customFields.propertyCharacteristics.propertyType }​}
{​{ project.customFields.propertyCharacteristics.numberOfBedrooms }​}<!-- Electrical system -->
{​{ project.customFields.electricalSystem.mainsVoltage }​}
{​{ project.customFields.electricalSystem.consumerUnitType }​}
{​{ project.customFields.electricalSystem.earthingSystem }​}<!-- Heat pump design -->
{​{ project.customFields.heatPumpDesignSpec.manufacturer }​}
{​{ project.customFields.heatPumpDesignSpec.modelNumber }​}
{​{ project.customFields.heatPumpDesignSpec.capacity }​}

Date formatting:

Use the date filter with format codes. See LiquidJS date documentation.

{​{ 'now' | date: "%d %B %Y" }​}               <!-- "17 November 2025" -->
{​{ 'now' | date: "%d/%m/%Y" }​}               <!-- "17/11/2025" -->
{​{ project.customFields.survey.surveyDate | date: "%d %B %Y" }​}

Common date format codes:

  • %d - Day (01-31)

  • %m - Month number (01-12)

  • %B - Full month name ("November")

  • %b - Short month ("Nov")

  • %Y - Four-digit year (2025)

  • %y - Two-digit year (25)

Conditionals:

Show content only when data exists:

{​% if customer.email %​}
  <p><strong>Email:</strong> {​{ customer.email }​}</p>
{​% endif %​}{​% if project.customFields.warranty.included %​}
  <div class="warranty-section">
    <h2>Warranty Information</h2>
    <p>Duration: {​{ project.customFields.warranty.duration }​} years</p>
  </div>
{​% endif %​}

Loops:

Iterate over lists or repeating data:

{​% for item in project.customFields.materials.items %​}
  <tr>
    <td>{​{ item.description }​}</td>
    <td>{​{ item.quantity }​}</td>
  </tr>
{​% endfor %​}

Learn more about LiquidJS syntax at LiquidJS Tags documentation.


Working with images

Critical: Upload fields are ALWAYS arrays

This is the most common mistake when creating templates. Every upload/image field in Payaca is an array, even if it only contains one image. You MUST loop through the array to display images.

Wrong approach (will not work):

<!-- DON'T DO THIS -->
<img src="{​{ project.customFields.survey.sitePhoto }​}" alt="Site Photo">

Correct approach:

<!-- CORRECT - Loop through array and use .url property -->
{​% if project.customFields.survey.sitePhoto %​}
  {​% for image in project.customFields.survey.sitePhoto %​}
  <img src="{​{ image.url }​}" alt="Site Photo">
  {​% endfor %​}
{​% endif %​}

Image properties available:

  • {​{ image.url }​} - Full resolution image (recommended for professional documents)

  • {​{ image.thumbnailUrl }​} - Smaller thumbnail version (faster loading, lower quality)

Recommended pattern: Clickable images with size constraints

This pattern creates professional-looking images that open full-size in a new tab when clicked:

{​% if project.customFields.survey.equipmentPhoto %​}
  <div class="image-field">
    {​% for image in project.customFields.survey.equipmentPhoto %​}
    <a href="{​{ image.url }​}" target="_blank" rel="noopener" class="image-link">
      <img src="{​{ image.url }​}" alt="Equipment Photo">
    </a>
    {​% endfor %​}
  </div>
{​% endif %​}

Required CSS for clickable images with size constraints:

<style>
  .image-field {
    margin: 1rem 0;
  }  .image-link {
    display: inline-block;
    cursor: pointer;
    border: 2px solid var(--border-color);
    border-radius: 4px;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
    overflow: hidden;
  }  .image-link:hover {
    border-color: var(--primary-color);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  }  .image-field img {
    max-width: 100%;
    max-height: 400px;
    width: auto;
    height: auto;
    object-fit: contain;
    display: block;
  }
</style>

Why these size constraints?

  • max-width: 100% - Prevents images wider than the container from overflowing

  • max-height: 400px - Prevents excessively tall images from dominating the page

  • object-fit: contain - Maintains aspect ratio while respecting constraints

  • Users can click to view full-resolution in new tab

Multiple images pattern (photo grid):

<div class="photo-grid">
  {​% for image in project.customFields.survey.propertyPhotos %​}
  <div class="photo-item">
    <a href="{​{ image.url }​}" target="_blank" rel="noopener">
      <img src="{​{ image.url }​}" alt="Property Photo">
    </a>
  </div>
  {​% endfor %​}
</div>

<style>
  .photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1rem;
  }  .photo-item img {
    width: 100%;
    height: 200px;
    object-fit: cover;
    border-radius: 4px;
  }
</style>


Responsive design

Use container queries, NOT media queries

Shadow DOM requires container queries for responsive behavior. Media queries only work for print styles.

Required setup:

<style>
  .document-container {
    container-type: inline-size;
    container-name: document;
  }
</style>

Container queries for responsive layouts:

<style>
  /* Base styles */
  .field-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
  }  /* Tablet and below - use container query */
  @container document (max-width: 768px) {
    .field-grid {
      grid-template-columns: repeat(2, 1fr);
    }
  }  /* Mobile - use container query */
  @container document (max-width: 480px) {
    .field-grid {
      grid-template-columns: 1fr;
    }
  }
</style>

Media queries ONLY for print:

<style>
  @media print {
    .document-container {
      padding: 0;
    }    .section-header {
      background: white;
      color: black;
      border: 2px solid var(--primary-color);
      print-color-adjust: exact;
      -webkit-print-color-adjust: exact;
    }    .page-break {
      page-break-after: always;
    }    a {
      text-decoration: none;
      color: inherit;
    }
  }
</style>

Auto-responsive grid pattern:

This pattern automatically adjusts columns based on available space:

<style>
  .field-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 1.5rem;
  }
</style>

  • Desktop: 3-4 columns (depending on screen width)

  • Tablet: 2 columns

  • Mobile: 1 column

  • Automatically adjusts without breakpoints!

Print-friendly color preservation:

<style>
  .section-header {
    background: var(--primary-color);
    color: white;
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }
</style>


Advanced patterns

Checkbox visualization

Display boolean/checkbox fields with visual checkboxes:

<div class="checkbox-field">
  <div class="checkbox-container">
    <div class="checkbox-box {​% if project.customFields.survey.workCompleted %​}checked{​% endif %​}">
      <span class="checkbox-checkmark">✓</span>
    </div>
  </div>
  <span class="checkbox-label">Work completed and tested</span>
</div>

<style>
  .checkbox-field {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 0.5rem 0;
  }  .checkbox-box {
    width: 24px;
    height: 24px;
    border: 2px solid var(--primary-color);
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: white;
  }  .checkbox-box.checked {
    background: var(--primary-color);
  }  .checkbox-checkmark {
    display: none;
    color: white;
    font-weight: bold;
    font-size: 18px;
  }  .checkbox-box.checked .checkbox-checkmark {
    display: block;
  }  .checkbox-label {
    font-size: 14px;
  }
</style>

Data tables

<table class="data-table">
  <thead>
    <tr>
      <th>Component</th>
      <th>Specification</th>
      <th>Quantity</th>
    </tr>
  </thead>
  <tbody>
    {​% for item in project.customFields.materials.components %​}
    <tr>
      <td>{​{ item.component }​}</td>
      <td>{​{ item.specification }​}</td>
      <td>{​{ item.quantity }​}</td>
    </tr>
    {​% endfor %​}
  </tbody>
</table>

<style>
  .data-table {
    width: 100%;
    border-collapse: collapse;
    margin: 1rem 0;
  }  .data-table th {
    background: var(--primary-color);
    color: white;
    padding: 0.75rem;
    text-align: left;
    font-weight: 600;
  }  .data-table td {
    padding: 0.75rem;
    border-bottom: 1px solid var(--border-color);
  }  .data-table tr:hover {
    background: #f8fafc;
  }
</style>

Signature boxes (coming soon)

Digital signature capture via the customer portal is currently in development. When available, customers will be able to sign documents directly through the portal, and signatures will appear automatically in your templates.

Signature boxes will display captured signatures with:

  • Signature image (captured via customer portal)

  • Signatory name

  • Date of signature

  • Placeholder line if not yet signed

This feature will be ideal for:

  • Commissioning certificate sign-off

  • Installation handover documentation

  • Customer acceptance forms

  • Warranty registration

Conditional sections

Show entire sections only when relevant:

{​% if project.customFields.heatPump.systemType == "air-source" %​}
<div class="section">
  <h2>Air Source Heat Pump Specifications</h2>
  <!-- ASHP specific content -->
</div>
{​% endif %​}{​% if project.customFields.solar.panelsInstalled %​}
<div class="section">
  <h2>Solar PV System Details</h2>
  <!-- Solar content -->
</div>
{​% endif %​}

Embedded company logo (base64)

For templates that need to work offline or without external dependencies:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." alt="Company Logo" style="height: 60px;">

Or use the dynamic account logo variable:

<img src="{​{ account.companyLogo }​}" alt="{​{ account.companyName }​}" style="height: 60px;">


Testing and troubleshooting

Before uploading to Payaca

  1. Check all field names are spelled correctly (case-sensitive)

  2. Verify custom field IDs match your account's fieldsets

  3. Test upload fields use array loop pattern

  4. Confirm Shadow DOM CSS rules (:host, not :root)

  5. Check container queries for responsive (not media queries)

  6. Validate HTML syntax

Common issues and solutions

Issue: Fields show as blank/undefined

  • ✓ Check spelling (case-sensitive)

  • ✓ Use project.customFields not customFieldValues

  • ✓ Remove .readable suffix (old syntax)

  • ✓ Verify fieldset and field IDs match your account

Issue: Images not displaying

  • ✓ Upload fields are arrays - must use {​% for image in field %​}

  • ✓ Use {​{ image.url }​} or {​{ image.thumbnailUrl }​} property

  • ✓ Wrap in conditional {​% if field %​}...{​% endif %​}

  • ✓ Check image field has data in project

Issue: Layout not responsive

  • ✓ Using container queries (@container), not media queries

  • .document-container has container-type: inline-size

  • ✓ Grid uses repeat(auto-fit, minmax(250px, 1fr)) pattern

Issue: Styles not applying

  • ✓ Using :host instead of :root

  • ✓ Using :host * instead of *

  • ✓ Using .document-container instead of body

  • ✓ All CSS is in inline <style> tag

Issue: Print layout broken

  • ✓ Using @media print for print-specific styles

  • ✓ Added print-color-adjust: exact to preserve colors

  • ✓ Using page-break-after: always for section breaks

Issue: Addresses not formatting

  • ✓ Use {​{ project.siteAddress }​} for auto-formatted address

  • ✓ Use {​{ account.address }​} for company address

  • ✓ Access components: postalCode not postcode, country not county

Testing checklist

  1. ✓ Preview in Payaca desktop view

  2. ✓ Preview in Payaca mobile view

  3. ✓ Generate document from real project with data

  4. ✓ Print preview (File → Print in browser)

  5. ✓ Check all images load and are clickable

  6. ✓ Verify all custom field values display correctly

  7. ✓ Test with project that has missing data (empty fields)

  8. ✓ Check conditional sections show/hide appropriately

Debugging techniques

Add temporary debug output to see what data is available:

<!-- Temporary debug section - remove before production -->
<div style="border: 2px solid red; padding: 1rem; margin: 1rem 0;">
  <p>DEBUG: Project Reference = {​{ project.reference }​}</p>
  <p>DEBUG: Customer Name = {​{ customer.name }​}</p>
  <p>DEBUG: Field Value = {​{ project.customFields.fieldset.field }​}</p>
</div>


Complete reference

Account variables (your company)

Variable

Description

Example Output

{​{ account.companyName }​}

Your company name

"ABC Renewables Ltd"

{​{ account.companyLogo }​}

Your company logo URL

"https://..."

{​{ account.address }​}

Auto-formatted company address

"123 High St, London, SW1A 1AA"

{​{ account.email }​}

Company email

{​{ account.phone }​}

Company phone

"01234 567890"

{​{ account.companyRegistrationNumber }​}

Registration number

"12345678"

Account address components

Variable

Description

Example Output

{​{ account.address['line1'] }​}

Address line 1

"123 High Street"

{​{ account.address['line2'] }​}

Address line 2

"Unit 4"

{​{ account.address.city }​}

City

"London"

{​{ account.address.postalCode }​}

Postcode

"SW1A 1AA"

{​{ account.address.country }​}

Country

"United Kingdom"

Project variables

Variable

Description

Example Output

{​{ project.reference }​}

Project reference number

"PROJ-2025-001"

{​{ project.siteAddress }​}

Auto-formatted site address

"45 Oak Lane, Bristol, BS1 1AA"

{​{ project.siteAddress['line1'] }​}

Site address line 1

"45 Oak Lane"

{​{ project.siteAddress['line2'] }​}

Site address line 2

"Clifton"

{​{ project.siteAddress.city }​}

Site city

"Bristol"

{​{ project.siteAddress.postalCode }​}

Site postcode

"BS1 1AA"

{​{ project.siteAddress.country }​}

Site country

"United Kingdom"

Customer variables

Variable

Description

Example Output

{​{ customer.name }​}

Customer name

"John Smith"

{​{ customer.primaryContact.name }​}

Primary contact full name

"John Smith"

{​{ customer.primaryContact.firstName }​}

Primary contact first name

"John"

Custom field variables

Variable

Description

Example Output

{​{ project.customFields.fieldsetId.fieldId }​}

Any custom field value

Varies by field type

{​{ project.customFields.survey.surveyDate }​}

Example: Survey date

"2025-11-17"

{​{ project.customFields.propertyInfo.propertyType }​}

Example: Property type

"Detached"

Date format codes

Code

Output

Example

%d

Day (01-31)

17

%m

Month number (01-12)

11

%B

Full month name

November

%b

Short month name

Nov

%Y

Four-digit year

2025

%y

Two-digit year

25

%A

Full day name

Monday

%a

Short day name

Mon

Common format combinations:

  • "%d %B %Y" → "17 November 2025"

  • "%d/%m/%Y" → "17/11/2025"

  • "%B %d, %Y" → "November 17, 2025"

  • "%d-%m-%Y" → "17-11-2025"

LiquidJS filters reference

Filter

Description

Example

date

Format date

{​{ 'now' | date: "%d/%m/%Y" }​}

upcase

Convert to uppercase

{​{ customer.name | upcase }​}

downcase

Convert to lowercase

{​{ project.reference | downcase }​}

capitalize

Capitalize first letter

{​{ description | capitalize }​}

default

Fallback if empty

{​{ field | default: "N/A" }​}

round

Round number

{​{ 3.14159 | round: 2 }​} → 3.14

replace

Replace text

{​{ text | replace: "old", "new" }​}

Field type reference

Field Type

How to Display

Example

Text

Direct output

{​{ project.customFields.fieldset.textField }​}

Number

Direct output

{​{ project.customFields.fieldset.numberField }​}

Date

Use date filter

{​{ field | date: "%d %B %Y" }​}

Boolean/Checkbox

Conditional

{​% if field %​}Yes{​% else %​}No{​% endif %​}

Upload/Image

Loop through array

{​% for image in field %​}{​{ image.url }​}{​% endfor %​}

Dropdown

Direct output

{​{ project.customFields.fieldset.dropdownField }​}

External resources


This guide covers all essential patterns and techniques for creating professional HTML document templates in Payaca. For additional support, contact Payaca support or consult the Payaca Help Center.

Did this answer your question?