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
- Go to Templates and click Create Template
- Choose HTML Editor as the template type
- Pick a starter template or start with the default boilerplate
- Write your HTML with Twig variables in the code editor
Twig syntax
Variables
Use double curly braces to output variables in your HTML:
<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.
<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:
{% 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:
{% 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:
{{ 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:
{% 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:
| Setting | Options | Default |
|---|---|---|
| Format | A0–A6, Letter, Legal, Tabloid, Ledger, or custom dimensions (mm) | A4 |
| Orientation | Portrait, Landscape | Portrait |
| Margins | Top, Right, Bottom, Left (mm) | 10 mm each |
| Custom dimensions | Width and Height in mm | Only 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:
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #999;">
<span>{{ company_name }}</span>
<span>{{ invoice_number }}</span>
</div><div style="text-align: center; font-size: 9px; color: #999;">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div><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.
{
"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-faceor Google Fonts@import - Variables — CSS custom properties (
var(--my-color)) - Print —
page-break-before,page-break-after,page-break-inside: avoid
<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:
{# 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
defaultfilter 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.
