Start for Free

HTML Editor

Build complex PDF layouts with HTML, CSS, and the Twig templating engine.

Overview

The HTML editor gives you full control over the PDF output by writing HTML and CSS. Dynamic data is injected using Twig syntax, a powerful templating language. The HTML is rendered to PDF using a headless browser, so modern CSS (flexbox, grid, etc.) is fully supported.

Editor layout

The editor is split into three panels:

  • Twig editor (left) — Monaco code editor with syntax highlighting for writing your HTML/Twig template
  • Preview data (top-right) — JSON editor for defining sample data used during preview
  • Live preview (bottom-right) — real-time HTML rendering that updates as you type (500ms debounce)

The header bar provides controls for page format, orientation, margins, and export.

Creating an HTML template

  1. Go to Templates and click Create Template
  2. Choose HTML Editor as the template type
  3. Pick a starter template or start with the default boilerplate
  4. Write your HTML with Twig variables in the code editor

Twig syntax

Variables

Use double curly braces to output variables in your HTML:

Variable outputhtml
<h1>Invoice {{ invoice_number }}</h1>
<p>Dear {{ customer_name }},</p>
<p>Amount due: {{ amount }}</p>
<p>Due date: {{ due_date }}</p>

Loops

Use for loops to iterate over arrays. This is the most common pattern for generating line items in invoices, rows in reports, etc.

Iterating over itemshtml
<table>
  <thead>
    <tr>
      <th>Description</th>
      <th>Qty</th>
      <th>Unit price</th>
      <th>Total</th>
    </tr>
  </thead>
  <tbody>
    {% for item in items %}
    <tr>
      <td>{{ item.description }}</td>
      <td>{{ item.quantity }}</td>
      <td>{{ item.unit_price }}</td>
      <td>{{ item.total }}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>

Inside a loop you have access to the loop variable:

Loop helpershtml
{% for item in items %}
  {{ loop.index }}      {# 1, 2, 3... #}
  {{ loop.index0 }}     {# 0, 1, 2... #}
  {{ loop.first }}      {# true on first iteration #}
  {{ loop.last }}       {# true on last iteration #}
  {{ loop.length }}     {# total number of items #}
{% endfor %}

Conditionals

Show or hide sections based on your data:

Conditional renderinghtml
{% if discount > 0 %}
  <tr class="discount">
    <td colspan="3">Discount ({{ discount }}%)</td>
    <td>-{{ discount_amount }}</td>
  </tr>
{% endif %}

{% if notes is not empty %}
  <div class="notes">
    <h3>Notes</h3>
    <p>{{ notes }}</p>
  </div>
{% endif %}

{% if status == 'paid' %}
  <span class="badge paid">Paid</span>
{% elseif status == 'overdue' %}
  <span class="badge overdue">Overdue</span>
{% else %}
  <span class="badge pending">Pending</span>
{% endif %}

Filters

Twig filters transform values. Chain them with the | (pipe) operator:

Common filtershtml
{{ name | upper }}                {# JANE DOE #}
{{ name | lower }}                {# jane doe #}
{{ name | capitalize }}           {# Jane doe #}
{{ name | title }}                {# Jane Doe #}
{{ price | number_format(2) }}    {# 1,200.00 #}
{{ date | date("d/m/Y") }}       {# 30/03/2026 #}
{{ description | default("N/A") }}
{{ html_content | raw }}          {# Output unescaped HTML #}
{{ items | length }}              {# Number of items #}
{{ text | trim }}                 {# Remove whitespace #}

Template inheritance

For complex documents, you can use Twig's set and macro features to create reusable components within your template:

Reusable macroshtml
{% macro currency(amount) %}
  <span class="currency">€{{ amount | number_format(2) }}</span>
{% endmacro %}

{% from _self import currency %}

<td>{{ currency(item.price) }}</td>
<td>{{ currency(total) }}</td>

Page configuration

Configure these settings in the editor header bar:

SettingOptionsDefault
FormatA0–A6, Letter, Legal, Tabloid, Ledger, or custom dimensions (mm)A4
OrientationPortrait, LandscapePortrait
MarginsTop, Right, Bottom, Left (mm)10 mm each
Custom dimensionsWidth and Height in mmOnly when format is "Custom"

Headers and footers

Define custom headers and footers that repeat on every page. They are written in HTML with Twig support and have access to special page variables:

Header examplehtml
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #999;">
  <span>{{ company_name }}</span>
  <span>{{ invoice_number }}</span>
</div>
Footer with page numbershtml
<div style="text-align: center; font-size: 9px; color: #999;">
  Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
Page variables
Use <span class="pageNumber"></span> and <span class="totalPages"></span> in headers/footers. These are automatically replaced with the current page number and total page count during rendering.

Preview data

The top-right panel is a JSON editor where you define sample data for live preview. This data is used only when previewing in the editor — it does not affect API calls.

Example preview datajson
{
  "company_name": "Acme Corp",
  "invoice_number": "INV-2026-042",
  "customer_name": "Jane Doe",
  "items": [
    { "description": "Web design", "quantity": 1, "unit_price": "€3,000", "total": "€3,000" },
    { "description": "Hosting (1 year)", "quantity": 1, "unit_price": "€120", "total": "€120" }
  ],
  "subtotal": "€3,120",
  "discount": 10,
  "discount_amount": "€312",
  "total": "€2,808",
  "due_date": "30/04/2026",
  "notes": "Payment due within 30 days.",
  "status": "pending"
}

Styling with CSS

Include CSS directly in your template with a <style> tag. The PDF renderer supports modern CSS including:

  • Layout — flexbox, CSS Grid
  • Typography — custom fonts via @font-face or Google Fonts @import
  • Variables — CSS custom properties (var(--my-color))
  • Print page-break-before, page-break-after, page-break-inside: avoid
Complete styled templatehtml
<style>
  :root {
    --primary: #1a1a2e;
    --accent: #e94560;
  }
  body {
    font-family: 'Helvetica Neue', sans-serif;
    color: var(--primary);
    font-size: 12px;
    line-height: 1.5;
  }
  .header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 40px;
  }
  table {
    width: 100%;
    border-collapse: collapse;
  }
  th {
    background: var(--primary);
    color: white;
    padding: 8px 12px;
    text-align: left;
  }
  td {
    padding: 8px 12px;
    border-bottom: 1px solid #eee;
  }
  .total {
    font-size: 18px;
    font-weight: bold;
    color: var(--accent);
  }
</style>

<div class="header">
  <div>
    <h1>Invoice {{ invoice_number }}</h1>
    <p>Date: {{ date }}</p>
  </div>
  <div style="text-align: right;">
    <strong>{{ company_name }}</strong><br>
    {{ company_address }}
  </div>
</div>

<table>
  <thead>
    <tr>
      <th>Description</th>
      <th>Qty</th>
      <th>Price</th>
    </tr>
  </thead>
  <tbody>
    {% for item in items %}
    <tr>
      <td>{{ item.description }}</td>
      <td>{{ item.quantity }}</td>
      <td>{{ item.price }}</td>
    </tr>
    {% endfor %}
  </tbody>
</table>

<div style="text-align: right; margin-top: 20px;">
  <span class="total">Total: {{ total }}</span>
</div>

Page breaks

Control where pages break in multi-page documents:

Page break controlshtml
{# Force a page break before this section #}
<div style="page-break-before: always;">
  <h2>Terms & Conditions</h2>
  ...
</div>

{# Prevent a table row from being split across pages #}
<tr style="page-break-inside: avoid;">
  <td>{{ item.description }}</td>
</tr>

Error handling

If your Twig template contains a syntax error, the editor highlights the error with the line number in the preview panel. Common errors include:

  • Unclosed tags: {% for ... %} without {% endfor %}
  • Undefined variables (use the default filter to handle missing data)
  • Invalid filter usage

WYSIWYG vs. HTML editor

Choose the HTML editor when:

  • You need multi-page documents with content that flows across pages
  • Your template has complex conditional logic or loops
  • You want pixel-perfect control with CSS
  • You're building reports, contracts, or data-heavy documents

Consider the WYSIWYG editor instead for simple fixed-layout templates or when you need to overlay fields on an existing PDF.