I was wrapping a Payload CMS admin customization for fresh-produce batches when the client asked for a printable checklist. The default admin UI has no print story, and opening a blank window.print() popup just gets blocked. After a few iterations I landed on a pattern that streams form data into a hidden iframe, prints immediately, and cleans itself up. This guide walks you through the exact setup so you can give editors a one-click printable production batch list.
The goal is to render whatever is on the React Hook Form (including unsaved edits) into a print-friendly HTML layout. We fall back to a Payload fetch when the form has no rows yet, but you could extend that to any other collection. Along the way you’ll see how the handler stays isolated from the component tree, so the same code can be reused from other actions or scripts.
1. Centralize printable helpers
Start by moving every formatting concern into a shared handler. This keeps the client component lean and makes it trivial to reuse the logic from other entry points or background jobs.
This handler takes care of formatting product labels, building a tiny HTML table, and handling the hidden iframe lifecycle. The iframe route avoids popup blockers because it stays in the same window context. You’re left with a single entry point (handlePrint) that any React component or background trigger can call.
2. Wire printable rows into the client form
Next, hydrate the handler from the production batch edit form. Build printable rows from whatever React Hook Form currently knows, and keep a summary count for editors so they can spot missing products before printing.
The buildPrintableProducts helper converts whatever is in the form—numbers, populated relationships, or blank rows—into a list that can be rendered for print. selectedProductCount is optional, but I like surfacing it in the details card so editors see how many items will land in the printout.
3. Hook into the admin toolbar
Finally, expose the print action next to the existing buttons. Disabling the button when there are no rows mirrors the iframe handler’s guard and keeps the UX honest.
With this in place the button runs entirely on the client. Editors get an instant print dialog, the iframe tears down automatically after afterprint, and unsaved changes are preserved. If you ever need to print straight from a list view—or from a scheduled script—you can call the same handler with data fetched via Payload’s REST or local API.
Wrap-up
The request started with “Can we print this batch?” and ended up with a reusable print pipeline that lives alongside the rest of our Payload admin customizations. We isolated every formatting decision in handlers/printable.ts, transformed live form state into printable rows, and surfaced a single button that opens the browser dialog without juggling extra tabs.
You now have everything you need to extend this pattern to other collections or expand the HTML template with logos and signatures. Let me know in the comments if you have questions, and subscribe for more practical development guides. Thanks, Matija