Tutorials
How to Print a PDF in Retool Using Custom Components
If you've been trying to figure out how to print a PDF in Retool, you've probably already hit the same wall: the built-in PDF Viewer has no print button, window.open() is blocked by the sandbox, and window.print() either does nothing or flashes a blank dialog. This post walks through a working solution using a custom component and Carbone.io, along with the built-in Retool utilities that can fill the gap in simpler cases.
Why Printing PDFs in Retool Is Complicated
Retool runs all JavaScript inside a sandboxed iframe for security reasons. That sandbox blocks most window functions — including window.open() and window.print() — which are exactly what you'd normally reach for when printing or opening a PDF in a new tab. The native PDF Viewer component doesn't expose a print action either. So out of the box, you're stuck.
The good news: there are two viable paths depending on your use case — a quick utility method for simple downloads, and a custom component approach for teams that need real print-dialog access.
The Quick Option: utils.downloadFile() and utils.downloadPage()
Retool's built-in utils.downloadFile() method can push a PDF to the user's browser as a download. It's not true printing, but it's the lowest-effort path if your users are okay with downloading first and printing from their file viewer.
utils.downloadFile(data, fileName, fileType)— triggers a browser download of a base64 or binary file.utils.downloadPage()— renders the current Retool page as a PDF download. Note: paginated tables will only show the first page, so you'll need to set your table to display all rows before calling this.
For high-volume workflows — like printing 200 invoices in a two-hour window — forcing a download step is a dealbreaker. That's where the custom component approach comes in.
How to Print a PDF in Retool Using a Custom Component and Carbone.io
This solution works by placing a custom component on your Retool app and enabling the "Allow popups to escape sandbox" setting. That flag is the key — it lets window.open() work inside your custom component's iframe, which means the PDF can open in a new browser tab where the user can print normally.
Step-by-Step Setup
- Step 1: Set up a Carbone.io account and create a template. Carbone uses a handlebars-style syntax to merge your data into a Word or LibreOffice document and render it as a PDF. Follow their docs to get your
templateIdand API auth token. - Step 2: Drag a Custom Component onto your Retool canvas. In its settings, enable "Allow popups to escape sandbox."
- Step 3: Edit the component model to pass in your query data, auth token, and template ID. Example model JSON:
{ "data": {{qryLineItemsSelect.data}}, "carboneAuthToken": {{carboneAuthToken.value}}, "carboneTemplateId": {{carboneInvoiceTemplate.value}} } - Step 4: Replace the default
MyCustomComponentin the component's script with a button that calls your print function on click:const MyCustomComponent = ({ triggerQuery, model, modelUpdate }) => ( <Button color="primary" variant="contained" size="small" fullWidth="true" onClick={() => { printInvoice(model.data, model.carboneAuthToken, model.carboneTemplateId) }} > Open Invoice </Button> ); - Step 5: Add the async
printInvoicefunction to the same script block. It POSTs to Carbone's render endpoint, gets back arenderId, then opens the rendered PDF in a new tab:async function printInvoice(data, authToken, templateId) { const resp = await fetch('https://render.carbone.io/render/' + templateId, { method: "POST", mode: 'cors', body: JSON.stringify({ "convertTo": 'pdf', "data": data }), headers: { "content-type": 'application/json', "Authorization": 'Bearer ' + authToken, "carbone-version": '3' } }).then(res => res.json()); if (resp && resp.success === true && resp.data && resp.data.renderId) { var newWindow = window.open(`https://render.carbone.io/render/${resp.data.renderId}`, '_blank'); newWindow.focus(); } else if (resp && resp.error) { return (resp.error); } }
When the user clicks the button, the PDF renders via Carbone and opens in a new browser tab. From there, they can use the browser's native print dialog (Ctrl+P / Cmd+P) to print it.
What About window.print() Inside the Custom Component?
Calling window.print() programmatically from within the custom component sandbox does not work reliably. In testing across Chrome, Edge, and Firefox, it either flashes a blank print dialog or does nothing. The workaround is to open the PDF in a new tab as shown above and let the user trigger the print from there. It's one extra click, but it's stable.
Controlling PDF Page Size (8.5x11, 4x6, etc.)
If your rendered PDF is coming out with incorrect dimensions — too wide, or not fitting a standard letter or index card size — the sizing is controlled at the template level in Carbone, not in Retool. Set your page margins, orientation, and paper size inside your Carbone template document before rendering. Retool itself has no mechanism to resize the PDF output after the fact.
The Bottom Line
Retool's native PDF printing support is minimal by design — the sandbox is a security feature, not an oversight. For straightforward use cases, utils.downloadFile() gets the job done. For production workflows where users need to print directly and quickly, the custom component approach with Allow popups to escape sandbox enabled is the most reliable path available today. It takes about 20 minutes to set up and works across all major browsers.
Ready to build?
We scope, design, and ship your Retool app — fast.