Dynamic tables are usually the first place where PDF templates break.
Why? Because array length changes, text length changes, and developers often hard-code assumptions for only one dataset.
In HookPDF, the robust pattern is:
- Keep table markup stable.
- Feed rows through
#each. - Keep JSON array shape consistent.
This article shows practical table patterns that stay predictable when data volume changes.
Flow diagram (array data -> dynamic rows -> PDF)
Template table markup
<tbody>{{#each usage_rows}} ... {{/each}}</tbody>
|
v
JSON payload
usage_rows: [ { ... }, { ... }, ... ]
|
v
POST /v1/render
|
v
job_id + status: queued
|
v
GET /v1/reports/{job_id}
|
v
status: completed -> output_url -> PDF
1) Base table pattern with #each
<table class="usage-table">
<thead>
<tr>
<th>Date</th>
<th>Event</th>
<th class="num">Count</th>
<th class="num">Credits</th>
</tr>
</thead>
<tbody>
{{#each usage_rows}}
<tr>
<td>{{date}}</td>
<td>{{event}}</td>
<td class="num">{{count}}</td>
<td class="num">{{credits}}</td>
</tr>
{{/each}}
</tbody>
</table>
This template does not need to change if the array has 2 rows or 200 rows.
2) Matching JSON shape
{
"usage_rows": [
{
"date": "2026-03-15",
"event": "invoice_render",
"count": 44,
"credits": 44
},
{
"date": "2026-03-16",
"event": "invoice_render",
"count": 38,
"credits": 38
},
{
"date": "2026-03-17",
"event": "summary_export",
"count": 12,
"credits": 12
}
]
}
The key rule is consistency. If template expects usage_rows, payload must always send usage_rows as an array.
3) Add stable table CSS for PDF output
.usage-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.usage-table th,
.usage-table td {
border-bottom: 1px solid #cbd5e1;
padding: 8px;
font-size: 12px;
}
.usage-table th {
background: #f8fafc;
text-align: left;
}
.usage-table td.num,
.usage-table th.num {
text-align: right;
white-space: nowrap;
}
Practical result:
- Numeric columns remain aligned.
- Large tables stay readable.
- Page breaks are cleaner than ad-hoc styles.
4) Real render request for dynamic table report
curl -X POST "https://api.hookpdf.com/v1/render" \
-H "Authorization: Bearer <api_key>" \
-H "Content-Type: application/json" \
-d '{
"template_id": "1c24f959-8e1a-4a14-acc4-cf57b4e60e99",
"payload": {
"report_title": "Weekly Usage Summary",
"period": "2026-03-11 to 2026-03-17",
"usage_rows": [
{
"date": "2026-03-15",
"event": "invoice_render",
"count": 44,
"credits": 44
},
{
"date": "2026-03-16",
"event": "invoice_render",
"count": 38,
"credits": 38
}
],
"totals": {
"events": 82,
"credits": 82
}
}
}'
Response:
{
"job_id": "8e70de8c-60fd-4f62-b9ec-2270a0c55f47",
"status": "queued",
"is_preview": false
}
Then check status with /v1/reports/{job_id} until completed.
5) Common mistakes with dynamic table data
- Sending object instead of array for
usage_rows. - Renaming payload keys without updating template variable paths.
- Mixing numeric and string formats in same column.
- Forgetting to right-align numeric cells.
6) Template review checklist for array-driven tables
- Is every table row generated from one predictable array key?
- Do row objects have a stable field schema?
- Are numeric columns right-aligned and consistent?
- Does template still look correct with very short and very long arrays?
Use this template for free now
Start with this template in HookPDF and render your first PDF for free.
Use this template freeRelated guides
Ready to move from prototype to production?
Create your account and generate your first API-driven PDF with HookPDF.
Get your API key