Two words that sound alike and mean different things.
Block design
Document template
Scope
One block
The whole document
Chosen in
The Designs tab of the inspector
The template picker in the toolbar
Changes
Style only
Structure, content, style, page settings
Stored as
block.layoutId
A complete document model
Block designs
Select a block and the Designs tab shows live thumbnails of every variant that block offers.
The counts vary by how much there is to say: a flagship data block like
lineItems has 12, each text block has 14, and pageBreak has 4
because there are only four meaningful ways to draw one.
Designs vary structure, not just size.
Hierarchy, borders, cards, panels, rules, bands, badges, density, image placement, column
structure, spacing. Ten variants that differ only in font size would be one design and nine
disappointments — so you will not find that here, and you should not ship it either.
The design list is one column, and it is not filtered.BlockLayout.category exists in the type and the five values are defined, but
nothing reads it yet — every design is listed, in declaration order, one per row with a
live preview of the real block. Set category if you like; it is recorded and
will group designs when filtering ships, and changes nothing today.
Adding your own design to a built-in block
addLayout appends one design to a block that already exists. Your brand's variant
appears in the same Designs tab, alongside the built-ins, with no other changes.
Your design sits beside the built-ins in the same tab, filtered under the category you gave it,
and applying it writes layoutId: 'acme-band' and nothing else.
Starting from a built-in design
Read the existing design, keep what works, change the rest. Cheaper and safer than guessing
which style keys a block understands.
const registry = createBuiltinRegistry()
// What does the built-in actually set?
const boxed = registry.get('signatureArea').layouts.find((l) => l.id === 'boxed')
console.log(boxed.style)
registry.addLayout('signatureArea', {
...boxed,
id: 'acme-boxed', // a NEW id — reusing one shadows the original
nameKey: 'Acme boxed',
style: { ...boxed.style, borderColor: '#0054a6' },
})
Discover the style keys instead of guessing them.registry.get(type).layouts and .defaultStyle tell you exactly which
properties a block responds to. A style key the block does not read is silently ignored —
which looks identical to a design that "does not work".
Replacing the whole design set
When the built-in designs are wrong for your product rather than merely incomplete:
Removing a design that documents already use.
A block whose layoutId no longer exists falls back to the block's default style —
it does not break, but it does change appearance. Keep the old id in the list, or accept that
existing documents will shift.
Designs and manual edits
Applying a design writes layoutId and nothing else. It never touches
content, so the words survive — and because manual overrides sit
above the design in style resolution, a hand-set colour survives too.
If the block has manual overrides, the builder asks.
Keep them, and the new design shows through everywhere the user did not intervene. Reset
them, and the design applies cleanly. What it never does is silently discard work someone
did by hand.
The flip side, and the cause of most "the design isn't working" reports:
if a user previously set a property by hand, a new design cannot change that property. It
is not broken — the override is winning, exactly as designed. "Reset style" clears it.
Document templates
17 built-in whole-document templates, covering the shapes real businesses print: minimal
business, modern professional, corporate, elegant, bold accent, compact commercial, service
invoice, retail/product, quotation, report, letter, certificate, international, dark header,
image-led product and clean monochrome.
Applying a template REPLACES the whole document, with no confirmation.
Rows, page settings, tokens, header, footer, cover and the document name — all of it,
even when the user has authored content. It is one undo step, so
Ctrl+Z is the only way back. If your users can lose work that way,
put your own confirmation in front of the toolbar button.
The picker shows names and descriptions, not previews.
There is no thumbnail, no category filter and no search, whatever
TemplateCategory suggests.
A template is a document, never a screenshot.
Applying one gives real blocks in real rows. Every part of it is selectable, editable,
draggable and deletable the moment it lands — because it is just a document like any other.
interface DocumentTemplate {
id: string
nameKey: LabelKey // a label key — but an unresolved key renders as itself,
descriptionKey?: LabelKey // so plain readable text is fine for your own templates
document: PdfDocument // a complete document
category?: TemplateCategory // recorded; the picker does not filter by it yet
tags?: string[]
thumbnailSvg?: string // declared, but the preview is always a live render
}
It is nameKey, not name.
Everything user-facing in the package is a label key. A key that no locale pack resolves falls
back to rendering itself, so nameKey: 'Acme invoice' displays exactly that — which
is why you can ignore the translation machinery for your own templates and still be consistent
with it when you need to.
Authoring one
Build it in the builder, take the JSON from onChange, and paste it in. There is no
separate template format to learn — a template is a document.
<PdfBuilder
templates={[
{
id: 'acme-invoice',
nameKey: 'Acme invoice',
descriptionKey: 'Our standard invoice, brand colours applied',
category: 'corporate',
document: savedDocumentJson, // exactly what onChange gave you
},
]}
/>
Extending a built-in template
Rather than starting from an empty page, take one of the 17 built-ins and change what you need.
This is usually the fastest way to a brand-consistent set.
import { createBuiltinTemplates } from '@storvexa/pdf-builder'
const builtins = createBuiltinTemplates()
const modern = builtins.find((t) => t.id === 'modern-professional')
const acmeModern = {
...modern,
id: 'acme-modern', // a NEW id — never reuse the built-in's
nameKey: 'Acme — modern',
document: {
...modern.document,
tokens: {
...modern.document.tokens, // keep everything you are not changing
primaryColor: '#0054a6',
fontFamily: "'Inter', sans-serif",
},
page: { ...modern.document.page, size: 'A4', margin: { top: 18, right: 16, bottom: 18, left: 16 } },
},
}
<PdfBuilder templates={[...builtins, acmeModern]} />
Spread, do not mutate.createBuiltinTemplates() hands you objects; editing them in place changes what
every other builder instance on the page sees. Spread each level you touch, as above.
Replacing a built-in with your version
Same id, your document — the picker shows yours and never the original.
builtinTemplates={false} plus your own list is how you take full control.
Leave it true (the default) and your templates are added to
the built-ins. Set it false and only your list is offered — which is what you want
when the built-ins would confuse your users or breach your brand guidelines.
Author templates with placeholder tokens, not real data.
A template is meant to be reused. If it contains "Acme Ltd, invoice INV-0042", every document
made from it starts with someone else's facts in it. Use
{{ customer.name }} and friends — see
Images & data.
Letting users save templates
Not built yet. Do not design around it.onSaveTemplate, onLoadTemplates and onDeleteTemplate
exist in the props type and nothing in the package calls them. There is no
“Save as template” button, dialog or menu item anywhere in the interface, so
passing these callbacks has no effect at all.
Until it ships, do it yourself — you already have everything you need:
// Your own button, outside the builder.
const saveAsTemplate = async () => {
const doc = builderRef.current.getDocument() // the ref handle from Quick start
await fetch('/api/templates', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: prompt('Template name'), document: doc }),
})
}
// Then feed them back in as ordinary templates.
<PdfBuilder templates={await loadMine()} />
The templates prop IS implemented — that is how your saved
documents get back into the picker. Only the save/load/delete callbacks are missing.