Onboarding a B2B client almost always starts with a document list: certificates, IDs, bank letters, proof of address, product photos. Agencies and consultancies usually send this as a PDF or a Word file, and the client prints it, loses it, and emails back asking which items are still missing.
A better option, and one that takes an afternoon to build, is a single static HTML page that works as an interactive checklist on screen, remembers progress between visits, and prints cleanly as a paper form. No framework, no backend, no login.
This post walks through the pattern: semantic markup, localStorage persistence, a progress bar, and print CSS.
Why a static page beats a PDF
- It can be linked, updated and versioned. When requirements change, every client sees the new list immediately instead of working from last year's PDF.
- Progress survives. The client can tick off items over several days on the same device.
- It's indexable. A public checklist page answers the exact questions prospects search for before they contact you.
- It still prints. Some clients (and some compliance teams) want paper. Print CSS handles that.
A nice live example is the Alibaba.com Hong Kong onboarding document checklist published by a Hong Kong agency: it groups the documents a company needs, lets visitors tick items off with progress saved in the browser, and has a print button for a paper copy. The rest of this post shows how to build the same kind of page yourself.
1. Markup: plain checkboxes with stable IDs
The single most important decision is giving every item a stable ID that doesn't depend on its position. If you key saved state by index and later insert an item in the middle, every saved tick shifts by one.
<main class="checklist">
<h1>Client onboarding documents</h1>
<div class="progress" aria-live="polite">
<div class="progress-bar"><span id="bar"></span></div>
<p id="progress-text">0 of 0 completed</p>
</div>
<section>
<h2>Company documents</h2>
<label><input type="checkbox" data-id="br-cert"> Business Registration Certificate</label>
<label><input type="checkbox" data-id="ci-cert"> Certificate of Incorporation</label>
<label><input type="checkbox" data-id="nar1"> Latest annual return (NAR1)</label>
</section>
<section>
<h2>Director and shareholder</h2>
<label><input type="checkbox" data-id="dir-id"> Director passport or ID card</label>
<label><input type="checkbox" data-id="dir-addr"> Director proof of address (within 3 months)</label>
</section>
<div class="actions">
<button type="button" id="print">Print</button>
<button type="button" id="reset">Reset</button>
</div>
</main>
Wrapping each checkbox in a <label> gives you a large click target and correct screen-reader behaviour for free. The aria-live region announces progress changes.
2. Persisting state with localStorage
Store an object keyed by data-id, and namespace the storage key with a version so you can invalidate old state if the list changes fundamentally.
const STORAGE_KEY = "onboarding-checklist:v1";
document.addEventListener("DOMContentLoaded", () => {
const boxes = [...document.querySelectorAll('input[type="checkbox"][data-id]')];
const bar = document.getElementById("bar");
const text = document.getElementById("progress-text");
function load() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
boxes.forEach(cb => { cb.checked = Boolean(saved[cb.dataset.id]); });
} catch {
// Corrupt or blocked storage: start fresh rather than crash.
}
}
function save() {
const state = Object.fromEntries(boxes.map(cb => [cb.dataset.id, cb.checked]));
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch {
// Private mode or quota exceeded: the page still works, it just won't remember.
}
}
function render() {
const done = boxes.filter(cb => cb.checked).length;
const pct = boxes.length ? Math.round((done / boxes.length) * 100) : 0;
bar.style.width = pct + "%";
text.textContent = `${done} of ${boxes.length} completed (${pct}%)`;
}
boxes.forEach(cb => cb.addEventListener("change", () => { save(); render(); }));
document.getElementById("print").addEventListener("click", () => window.print());
document.getElementById("reset").addEventListener("click", () => {
if (!confirm("Clear all ticked items?")) return;
boxes.forEach(cb => { cb.checked = false; });
save();
render();
});
load();
render();
});
A few things worth calling out:
-
Wrap storage access in
try/catch. Safari private mode and some corporate browser policies throw onsetItem. The checklist should degrade to "works, doesn't remember", never to "broken". - Ignore unknown keys. Because we only read IDs that exist in the DOM, removing an item from the list leaves harmless stale data rather than errors.
- Confirm destructive actions. A reset button next to a print button will get clicked by accident.
3. Print CSS that produces a real form
On paper, the progress bar and buttons are noise, and the browser's default checkbox rendering is tiny. A short print stylesheet fixes both:
@media print {
.actions,
.progress { display: none; }
body { font-size: 11pt; color: #000; }
section { break-inside: avoid; }
h2 { break-after: avoid; }
label {
display: block;
padding: 4pt 0;
border-bottom: 0.5pt solid #ccc;
}
input[type="checkbox"] {
appearance: none;
width: 11pt;
height: 11pt;
border: 1pt solid #000;
vertical-align: middle;
margin-right: 6pt;
}
input[type="checkbox"]:checked {
background: #000;
}
@page { margin: 15mm; }
}
break-inside: avoid on each section stops a category heading from ending up alone at the bottom of a page. Drawing checkboxes as bordered squares means clients who print a blank copy get boxes they can tick with a pen, while anyone who already ticked items online gets them printed filled in.
4. Small touches that matter
Deep-linkable sections. Give each <section> an id so you can email a client a link straight to "Director and shareholder" when that's the only thing missing.
A "last updated" date. Requirements for bank accounts, platform onboarding and compliance change. Showing the revision date on the page (and in print) prevents disputes about which version a client followed.
Structured data, carefully. If the page is genuinely a step-by-step process, HowTo markup can describe it; for a plain list of documents, an ItemList or simply good headings is more honest. Don't mark up content that isn't visible.
Bilingual versions. If your clients split between languages, publish one page per language with matching data-ids. Since the IDs are language-neutral, you could even share the storage key so a client who switches language keeps their progress.
When to add a backend
This pattern intentionally stores nothing server-side, which is a feature: no personal data leaves the client's browser. Move to a backend only when you actually need one — for example, if your team needs to see which documents a specific client has ticked, or files need to be uploaded against each item. At that point the same stable IDs become your database keys, so the static version isn't wasted work.
Wrap-up
A static, interactive, printable checklist is one of the highest-leverage pages a service business can publish. It reduces back-and-forth email, answers pre-sales questions, and ranks for the exact "what documents do I need" searches prospects make. The whole thing is about 60 lines of JavaScript and 30 lines of CSS. Build one for whatever your clients most often ask you to send them a list of.
Top comments (0)