Getting Started
5-second setup
No npm. No bundler. The easiest option is to load Stapler directly from the CDN:
<script src="https://cdn.jsdelivr.net/gh/rlnorthcutt/stapler@main/dist/stapler.min.js"></script>
Or download stapler.min.js and host it yourself. Either way, all custom elements register themselves immediately when the script runs.
That's it. You're done installing. Write a page:
<stapled-doc page-width="8.5in" page-height="11in">
<s-page>
<s-page-body style="padding: 2rem;">
<h1>Hello, paper.</h1>
</s-page-body>
</s-page>
</stapled-doc>
The Anatomy of a Staple
Every Stapler document has the same basic structure: a root wrapper that sets the page dimensions, and page content inside it.
<stapled-doc> is the root. It reads your attributes, measures things, and does all the real work. Think of it as the stapler itself.
<s-page> is a single page — a hard-clipped, fixed-size container. Content that overflows is hidden. This is intentional: if something doesn't fit on a page, it should be on a different page.
Headers, footers, page breaks, and page numbers are all declared once inside <stapled-doc>. The library handles the repetition.
Attributes Reference
<stapled-doc>
| Attribute | Type | Default | Description |
|---|---|---|---|
page-width |
CSS length | 8.5in |
Width of each page. Accepts px, in, cm, mm, pt, rem, em. Strongly recommended; if omitted, logs a console error and falls back to 8.5in. |
page-height |
CSS length | 11in |
Height of each page. Same units as page-width. Strongly recommended; if omitted, logs a console error and falls back to 11in. |
page-gap |
CSS length | 2rem |
Gap between pages in the document view. Not printed. |
preview |
print |
none | Set to print to collapse page-gap to 0 on screen, previewing how the document will look when printed. |
embed |
boolean | none | Attaches a shadow root to <stapled-doc> and moves its children into it, isolating the document's CSS from the host page. The one place this library uses Shadow DOM. |
stylesheet |
comma-separated URLs | none | Only used with embed. One or more stylesheet URLs to inject into the shadow root, e.g. stylesheet="a.css, b.css". |
<page-header> and <page-footer>
Defined once as direct children of <stapled-doc>. Cloned into every page automatically. Style them with your own CSS — the library adds no visual styles.
| Attribute | Type | Default | Description |
|---|---|---|---|
height |
CSS length | 24px |
Strongly recommended. If omitted, logs a console error and falls back to 24px so the document still builds. |
skip-first |
boolean | false | Suppress on page 1. Useful for cover pages. |
skip-pages |
"1,3,5" |
— | Comma-separated, 1-indexed page numbers to suppress. |
<s-page>
| Attribute | Type | Default | Description |
|---|---|---|---|
skip-header |
boolean | false | Suppress the header on this specific page. |
skip-footer |
boolean | false | Suppress the footer on this specific page. |
page-width |
CSS length | inherited | Per-page override. Use for landscape inserts. |
page-height |
CSS length | inherited | Per-page height override. |
How It Works
Each <s-page> is a fixed-size, hard-clipped box sized from your page-width and page-height attributes. What you put in is what you get — content that overflows is hidden. You control exactly what goes on every page.
<stapled-doc page-width="8.5in" page-height="11in" page-gap="2rem">
<page-header height="40px" skip-first>
<div>My Document · <page-number format="n of total"></page-number></div>
</page-header>
<page-footer height="24px">
<div>Confidential</div>
<div>© Acme Corp</div>
</page-footer>
<s-page skip-header skip-footer>
<!-- Cover page — no header or footer -->
<s-page-body style="padding: 3rem;">
<h1>My Document</h1>
</s-page-body>
</s-page>
<s-page>
<!-- Page 2 — header and footer stamped automatically -->
<s-page-body style="padding: 2rem;">
<p>Content here.</p>
</s-page-body>
</s-page>
<s-page page-width="11in" page-height="8.5in">
<!-- Landscape insert — per-page size override -->
</s-page>
</stapled-doc>
page-height − header-height − footer-height.
Stamping Headers & Footers
Define your header and footer once, directly inside <stapled-doc>. The library clones them into every <s-page> automatically, then removes the originals from the DOM.
Style with plain CSS — the library sets no colors, fonts, or padding on your header and footer content:
page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1.5rem;
background: #1a1a2e;
color: white;
font-size: 10px;
}
page-footer {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 0 1.5rem;
border-top: 1px solid #e5e7eb;
font-size: 10px;
color: #9ca3af;
}
Pagination
Use <page-number> inside any header or footer template. The library resolves it when the template is stamped — each clone gets the correct page number.
format value |
Output on page 3 of 8 |
|---|---|
n (default) |
3 |
n/total |
3 / 8 |
n of total |
3 of 8 |
total |
8 |
<page-header height="36px">
<span>My Report</span>
<span>Page <page-number format="n of total"></page-number></span>
</page-header>
Dynamic Refresh
Call refresh() after any meaningful DOM change — injected content, user interaction that changes page height, font swap. The library re-runs the full build sequence.
const doc = document.querySelector('stapled-doc')
// After changing content:
doc.refresh()
// Listen for completion:
doc.addEventListener('sp:ready', (e) => {
console.log(e.detail)
// { pageCount: 3, pageWidth: 816, pageHeight: 1056 }
})
The sp:ready event fires after every build, including the initial build and every refresh() call. It bubbles.
Printing to PDF
There are two scenarios, and they want different tools.
Standalone documents
When the stapler doc is the page (open directly in a browser, or the sole document inside an iframe), window.print() is the right call. Open in Chrome or Edge, press Cmd/Ctrl+P, and set:
- Paper size: match your
page-width×page-height - Margins: None
- Background graphics: On
Add this to your document to pin the page size for headless Chrome:
@page {
size: 8.5in 11in; /* match page-width × page-height */
margin: 0;
}
For programmatic PDF generation:
chrome --headless --print-to-pdf=output.pdf --no-margins document.html
If the stapler doc lives inside an iframe on a host page, have the parent post a message and let the iframe call window.print() on itself:
// parent
iframe.contentWindow.postMessage({ type: 'print' }, '*');
// inside the iframe
window.addEventListener('message', (e) => {
if (e.data?.type === 'print') window.print();
});
The iframe owns its <head>, its stylesheets, and its @page rules, so it has to be the one to drive the dialog.
Embedded in a larger page
When <stapled-doc> is one element inside a larger host page, window.print() prints the entire host page, which is rarely what you want. Use the companion <print-element-button> custom element. It prints just the target element in an isolated iframe and clones the host document's <head> so the stapler's styles travel with it.
Drop in the script:
<script type="module" src="https://cdn.jsdelivr.net/gh/rlnorthcutt/print-element-button@main/dist/print-element-button.min.js"></script>
Then add a button targeting your stapler:
<print-element-button target="stapled-doc" page-size="8.5in 11in" margins="0">
Print Document
</print-element-button>
The page-size and margins attributes get written straight into the print iframe's @page rule, so the stapler doc prints at its declared dimensions without margin chrome.
Why two tools?
They solve different problems. <print-element-button> means "print this element from my document." It borrows the host page's styles and renders the target in isolation. Iframe-driven window.print() means "tell that document to print itself." The iframe already has its own head and styles, so it should drive its own dialog. Mixing the two either breaks on cross-origin iframes or strips the wrong styles.
Using with AI
Stapler is designed to work well with LLMs. The markup is unambiguous, the attributes have explicit contracts, and there is no layout engine for the model to reason about — it just emits pages and the library handles the rest.
The quickest way to get started is to paste the Stapler AI Prompt into your conversation. It gives the model everything it needs: the script tag, element structure, attribute rules, and a complete working example.
View the AI prompt on GitHub →
Key rules for AI-generated documents
- Always include the CDN
<script>tag in<head>. page-widthandpage-heightare required on<stapled-doc>.- Always set
heightexplicitly on<page-header>and<page-footer>— this skips a measurement pass and is more reliable. - Wrap page content in
<s-page-body>and set padding via inline style. - Content that overflows a page is clipped. The model must fit content within
page-height − header-height − footer-height. - Use
skip-header skip-footeron a cover<s-page>andskip-firston<page-header>to suppress the header on page 1.
Minimal prompt to include in your conversation
Generate an HTML document using the Stapler web component library.
Load it from the CDN:
<script src="https://cdn.jsdelivr.net/gh/rlnorthcutt/stapler@main/dist/stapler.min.js"></script>
Use <stapled-doc page-width="8.5in" page-height="11in"> as the root.
Each page is an <s-page> with an <s-page-body style="padding: 2rem;"> inside.
Define <page-header height="40px"> and <page-footer height="24px"> once inside the root.
Use <page-number format="n of total"></page-number> inside the header.
The cover page should have skip-header skip-footer on its <s-page>.
Add skip-first to <page-header> so the header is suppressed on page 1.
Add @page { size: 8.5in 11in; margin: 0; } to the stylesheet.
Content that overflows a page is clipped — fit all content within each page.