Appearance
Custom Blocks
The custom block type lets you extend renderTemplate with arbitrary drawing when built-in block types don't fit your use case.
The custom block type
Each custom block must provide two callbacks:
estimate()— returns the vertical space (in points) the block needs. Called during pagination to determine where page breaks fall.render(ctx)— draws the block onto the page.ctxexposesx,y,width, and drawing helpers likegraphics().
ts
{
type: "custom",
height: 12,
estimate() { return this.height; },
render(ctx) {
ctx.graphics()
.moveTo(ctx.x, ctx.y + ctx.height / 2)
.lineTo(ctx.x + ctx.width, ctx.y + ctx.height / 2)
.stroke({ color: rgb(0.6, 0.6, 0.6) });
}
}height is the block's declared height; estimate() can return a different value if the content doesn't fill the full height.
Horizontal rule
A common reusable pattern:
ts
function hr(height = 2, color = rgb(0.7, 0.7, 0.7)) {
return {
type: "custom" as const,
height,
estimate() { return height; },
render(ctx) {
ctx.graphics()
.rect(ctx.x, ctx.y, ctx.width, height)
.fill({ color });
}
};
}
// Usage
doc.renderTemplate({
blocks: [
{ type: "heading", text: "Section One" },
{ type: "paragraph", text: "Content..." },
hr(),
{ type: "heading", text: "Section Two", options: { level: 2 } }
]
});Page break with custom text
Combine a page break with a decorative separator:
ts
function sectionBreak(label: string) {
return {
type: "custom" as const,
height: 28,
estimate() { return 28; },
render(ctx) {
const midY = ctx.y + ctx.height / 2;
ctx.graphics()
.moveTo(ctx.x, midY)
.lineTo(ctx.x + ctx.width, midY)
.stroke({ color: rgb(0.5, 0.5, 0.5) });
// Draw label using the page's current font context
}
};
}Watermark
Draw a watermark behind content on every page using the header callback:
ts
doc.renderTemplate({
blocks: [ /* body content */ ],
header: ({ pageNumber, totalPages }) => [{
type: "custom",
height: 0,
estimate() { return 0; },
render(ctx) {
// Only on page 1; render under body by drawing in header pass
}
}]
});Limitations
- No paging context during
render.pageNumberandtotalPagesare not available in body-level custom blocks. Use the header/footer callbacks if you need page-aware drawing. - No automatic height inference. You must supply
estimate()explicitly. - No structure tagging. Custom blocks do not create entries in the tagged structure tree. Use
sectionblocks with aroleif you need semantic structure.