Render When Ready
render_when_readyTrueCreate a multi-page business account statement from JSON data using vanilla JavaScript, reusable HTML templates, and print-ready CSS.
This example creates a multi-page business account statement from a JSON response. It fetches a generated 60-day ledger from the sample account statement endpoint, then uses vanilla JavaScript and an HTML <template> to populate the account summary, balances, and transaction rows before rendering the PDF.
Keeping the sample data outside the HTML makes the document easier to understand and closer to a real integration. You can replace the example endpoint with your own API while keeping most of the statement layout and rendering request unchanged.
The finished statement includes account details, the statement period, opening and closing balances, credit and debit totals, categorized transactions, and account notices. Print-specific styles keep the transaction table readable as it continues across multiple pages.

Interactive example
Change the HTML, CSS, or API options. The browser preview updates as you edit; select PDF to render the final output through the API.
Example code
Choose a language and copy a complete request using this example's HTML, CSS, and API options.
// dotnet add package HtmlCssToImage
using HtmlCssToImage;
using HtmlCssToImage.Models;
using HtmlCssToImage.Models.Requests;
using System.Collections.Generic;
using System.Text.Json.Nodes;
// In an application, use IHttpClientFactory through dependency injection:
// https://www.nuget.org/packages/HtmlCssToImage.DependencyInjection
using var httpClient = new HttpClient();
var hctiOps = new HtmlCssToImageOptions(Environment.GetEnvironmentVariable("HCTI_API_ID")!, Environment.GetEnvironmentVariable("HCTI_API_KEY")!);
var client = new HtmlCssToImageClient(httpClient, hctiOps);
var request = new CreateHtmlCssImageRequest
{
Html = """
<p id="loading" class="loading">Loading account statement…</p>
<main id="statement" class="statement" hidden>
<header class="statement-header">
<div class="brand">
<div class="brand-mark">M</div>
<div>
<strong data-institution-name></strong>
<span>Business banking</span>
</div>
</div>
<div class="statement-heading">
<span>Account statement</span>
<strong data-statement-period></strong>
<small data-statement-id></small>
</div>
</header>
<section class="account-details">
<div>
<span>Prepared for</span>
<strong data-account-name></strong>
</div>
<div>
<span>Account</span>
<strong data-account-type></strong>
</div>
<div>
<span>Account number</span>
<strong data-account-number></strong>
</div>
</section>
<section class="summary">
<article class="summary-primary">
<span>Closing balance</span>
<strong data-closing-balance></strong>
</article>
<article>
<span>Opening balance</span>
<strong data-opening-balance></strong>
</article>
<article>
<span>Credits</span>
<strong class="credit" data-total-credits></strong>
</article>
<article>
<span>Debits</span>
<strong data-total-debits></strong>
</article>
</section>
<section class="activity">
<div class="section-heading">
<div>
<span>Account activity</span>
<h1>Transactions</h1>
</div>
<p data-transaction-count></p>
</div>
<table>
<thead>
<tr>
<th>Date</th>
<th>Description</th>
<th class="amount">Amount</th>
</tr>
</thead>
<tbody id="transactions"></tbody>
</table>
</section>
<footer class="statement-footer">
<div>
<strong>Questions about this statement?</strong>
<span data-support></span>
</div>
<ul id="notices"></ul>
</footer>
</main>
<template id="transaction-template">
<tr>
<td data-date></td>
<td class="transaction">
<strong data-description></strong>
<span data-category></span>
</td>
<td class="amount" data-amount></td>
</tr>
</template>
<template id="notice-template">
<li data-notice></li>
</template>
<script>
const endpoint =
"https://example-data.htmlcsstoimage.com/account-statement";
async function renderStatement() {
const response = await fetch(endpoint);
const data = await response.json();
const money = cents =>
new Intl.NumberFormat(data.account.locale, {
style: "currency",
currency: data.account.currency
}).format(cents / 100);
const date = value =>
new Intl.DateTimeFormat(data.account.locale, {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC"
}).format(new Date(`${value}T00:00:00Z`));
document.querySelector("[data-institution-name]").textContent =
data.institution.name;
document.querySelector("[data-statement-period]").textContent =
`${date(data.statement.period_start)}–${date(data.statement.period_end)}`;
document.querySelector("[data-statement-id]").textContent =
data.statement.statement_id;
document.querySelector("[data-account-name]").textContent =
data.account.name;
document.querySelector("[data-account-type]").textContent =
data.account.account_type;
document.querySelector("[data-account-number]").textContent =
data.account.account_number_masked;
document.querySelector("[data-closing-balance]").textContent =
money(data.summary.closing_balance_cents);
document.querySelector("[data-opening-balance]").textContent =
money(data.summary.opening_balance_cents);
document.querySelector("[data-total-credits]").textContent =
`+${money(data.summary.total_credits_cents)}`;
document.querySelector("[data-total-debits]").textContent =
money(data.summary.total_debits_cents);
document.querySelector("[data-transaction-count]").textContent =
`${data.meta.transaction_count} transactions`;
document.querySelector("[data-support]").textContent =
`${data.institution.support_email} · ${data.institution.support_phone}`;
const transactionTemplate =
document.querySelector("#transaction-template");
const transactions = document.createDocumentFragment();
for (const transaction of data.transactions) {
const row =
transactionTemplate.content.firstElementChild.cloneNode(true);
row.querySelector("[data-date]").textContent =
date(transaction.posted_on);
row.querySelector("[data-description]").textContent =
transaction.description;
row.querySelector("[data-category]").textContent =
transaction.category;
const amount = row.querySelector("[data-amount]");
amount.textContent = money(transaction.amount_cents);
amount.classList.add(transaction.direction);
transactions.append(row);
}
document.querySelector("#transactions").append(transactions);
const noticeTemplate = document.querySelector("#notice-template");
const notices = document.createDocumentFragment();
for (const message of data.notices) {
const notice =
noticeTemplate.content.firstElementChild.cloneNode(true);
notice.textContent = message;
notices.append(notice);
}
document.querySelector("#notices").append(notices);
document.querySelector("#loading").remove();
document.querySelector("#statement").hidden = false;
requestAnimationFrame(() => ScreenshotReady());
}
renderStatement();
</script>
""",
Css = """
* {
box-sizing: border-box;
}
html {
color: #172033;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
font-size: 14px;
}
body {
margin: 0;
padding: 32px;
background: #eef2f6;
}
.statement {
width: 100%;
max-width: 7.5in;
margin: 0 auto;
padding: 36px;
background: white;
border-radius: 12px;
box-shadow: 0 12px 36px rgba(23, 32, 51, 0.12);
}
.loading {
margin: 0;
color: #64748b;
}
.statement-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding-bottom: 24px;
border-bottom: 2px solid #172033;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-mark {
display: grid;
width: 40px;
height: 40px;
place-items: center;
color: white;
background: #3157d5;
border-radius: 10px;
font-size: 20px;
font-weight: 800;
}
.brand strong,
.brand span,
.statement-heading span,
.statement-heading strong,
.statement-heading small {
display: block;
}
.brand strong {
font-size: 15px;
}
.brand span,
.statement-heading span,
.statement-heading small {
margin-top: 3px;
color: #64748b;
font-size: 11px;
}
.statement-heading {
text-align: right;
}
.statement-heading span {
margin: 0 0 5px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.statement-heading strong {
font-size: 15px;
}
.account-details {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
gap: 24px;
padding: 22px 0;
}
.account-details span,
.summary span {
display: block;
margin-bottom: 6px;
color: #64748b;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.account-details strong {
font-size: 13px;
}
.summary {
display: grid;
grid-template-columns: 1.4fr repeat(3, 1fr);
overflow: hidden;
margin-bottom: 32px;
border: 1px solid #dbe2ea;
border-radius: 10px;
}
.summary article {
min-height: 82px;
padding: 17px;
border-left: 1px solid #dbe2ea;
}
.summary article:first-child {
border-left: 0;
}
.summary strong {
font-size: 15px;
}
.summary-primary {
color: white;
background: #172033;
}
.summary-primary span {
color: #b8c3d4;
}
.summary-primary strong {
font-size: 21px;
}
.credit {
color: #16805b;
}
.section-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 13px;
}
.section-heading span {
color: #3157d5;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.section-heading h1 {
margin: 3px 0 0;
font-size: 20px;
}
.section-heading p {
margin: 0;
color: #64748b;
font-size: 11px;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
display: table-header-group;
}
th {
padding: 10px 8px;
color: #64748b;
background: #f5f7fa;
border-bottom: 1px solid #dbe2ea;
font-size: 10px;
letter-spacing: 0.07em;
text-align: left;
text-transform: uppercase;
}
td {
padding: 10px 8px;
border-bottom: 1px solid #e7ebf0;
font-size: 11px;
vertical-align: top;
}
tr {
break-inside: avoid;
}
th:first-child,
td:first-child {
width: 106px;
}
.amount {
width: 105px;
text-align: right;
white-space: nowrap;
}
.transaction strong,
.transaction span {
display: block;
}
.transaction span {
margin-top: 3px;
color: #718096;
font-size: 9px;
}
td.credit {
color: #16805b;
font-weight: 700;
}
.statement-footer {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 30px;
margin-top: 28px;
padding-top: 20px;
border-top: 2px solid #172033;
break-inside: avoid;
}
.statement-footer strong,
.statement-footer span {
display: block;
}
.statement-footer span,
.statement-footer li {
color: #64748b;
font-size: 9px;
line-height: 1.55;
}
.statement-footer ul {
margin: 0;
padding-left: 16px;
}
@media print {
body {
padding: 0;
background: white;
}
.statement {
max-width: none;
padding: 0;
border-radius: 0;
box-shadow: none;
}
}
""",
RenderWhenReady = true,
PDFOptions = new PDFOptions
{
PageHeight = new PdfValueWithUnits(11m, PdfUnit.INCHES),
PageWidth = new PdfValueWithUnits(8.5m, PdfUnit.INCHES),
Scale = 1m,
Margins = new PdfMargins(
new PdfValueWithUnits(0.4m, PdfUnit.INCHES),
new PdfValueWithUnits(0.4m, PdfUnit.INCHES),
new PdfValueWithUnits(0.4m, PdfUnit.INCHES),
new PdfValueWithUnits(0.4m, PdfUnit.INCHES)),
PrintBackground = true,
},
MediaType = MediaType.print,
};
using var result = await client.CreateImageAsync(request);
if (result.Success)
{
Console.WriteLine(result.Response.Url);
}
Request configuration
These values are sent with the HTML and CSS to control how HCTI loads the page and captures the final PDF. You can change each one in the editor above.
render_when_readyTruemedia_typeprintpdf_optionsHCTI.PDFOptionsV2From code to screenshot
HTML/CSS to Image renders HTML and CSS in hosted Chrome, then captures the result as an image or PDF. Each example combines real source code with the API options used for the render, so it is both a working demo and a starting point for your own request.
Edit the source and options above to test changes in the browser, then render through the API to see the production result.
Yes. Open an example to inspect and edit its complete HTML, CSS, and render options. Use it as a starting point, then send the resulting request from your application.
Examples are complete API requests built from HTML and CSS. Templates are saved, reusable designs created in the visual editor and rendered repeatedly with different variable values.
Yes. HCTI can render HTML and CSS or capture a public webpage as an image, and it can generate PDFs when you need document output instead of a screenshot.
Send the example’s HTML, CSS, and request options from your application.
Use HCTI with MCP-enabled AI tools, Zapier, Make, and other automation platforms.
Start with a visual preset, customize it in the template editor, and render it repeatedly with dynamic values.
Keep exploring
Social card with a fun SVG background and highlighted text.
Explore example