How to Display a Base64 PDF in an HTML Iframe
Embed a Base64 PDF with a data URL, add a reliable download fallback, and troubleshoot sizing, CSP, sandbox, and large-file issues.
A browser can display a Base64-encoded PDF in an <iframe> by using a PDF data URL as the frame’s src. The essential format is data:application/pdf;base64,<payload>. Give the iframe an explicit size and provide a separate download or open link because an iframe has no dependable child fallback when the browser cannot render the PDF.
Minimal working example
Replace BASE64_PDF_DATA with the Base64 representation of the PDF’s binary bytes:
<iframe
src="data:application/pdf;base64,BASE64_PDF_DATA"
width="100%"
height="600"
title="PDF preview"
></iframe>
<a
href="data:application/pdf;base64,BASE64_PDF_DATA"
download="document.pdf"
>
Download the PDF
</a>
The data: URL follows the syntax defined by RFC 2397: metadata, a comma, and the data payload. The media type is application/pdf; base64 tells the browser how to decode the payload.
How the embedding works
- Obtain the PDF bytes from a file, API response, database, or upload.
- Encode those bytes as Base64.
- Check whether the value already contains a
data:application/pdf;base64,prefix. - Use exactly one prefix followed by the Base64 payload.
- Assign the resulting URL to the iframe and to a separate link.
Do not Base64-encode a filename, a URL, or an already-prefixed data URL. The payload must represent the PDF bytes themselves.
Complete browser example with input validation
This example accepts a local PDF file, converts it to a data URL, and displays it. It also keeps a download link outside the frame.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Base64 PDF preview</title>
<style>
iframe { width: 100%; height: 70vh; min-height: 480px; border: 1px solid #ccc; }
#download { display: inline-block; margin-top: 0.75rem; }
#error { color: #b00020; }
</style>
</head>
<body>
<label>
Choose a PDF:
<input id="file" type="file" accept="application/pdf">
</label>
<p id="error" role="alert"></p>
<iframe id="preview" title="PDF preview" hidden></iframe>
<a id="download" download="document.pdf" hidden>Download the PDF</a>
<script>
const fileInput = document.querySelector('#file');
const frame = document.querySelector('#preview');
const download = document.querySelector('#download');
const error = document.querySelector('#error');
fileInput.addEventListener('change', () => {
error.textContent = '';
frame.hidden = true;
download.hidden = true;
const file = fileInput.files[0];
if (!file) return;
if (file.type !== 'application/pdf') {
error.textContent = 'Select a PDF file.';
return;
}
const reader = new FileReader();
reader.onerror = () => {
error.textContent = 'The file could not be read.';
};
reader.onload = () => {
const dataUrl = reader.result;
if (typeof dataUrl !== 'string' ||
!dataUrl.startsWith('data:application/pdf;base64,')) {
error.textContent = 'The file did not produce a PDF data URL.';
return;
}
frame.src = dataUrl;
download.href = dataUrl;
frame.hidden = false;
download.hidden = false;
};
reader.readAsDataURL(file);
});
</script>
</body>
</html>
Normalize values from APIs or databases
Inputs commonly arrive in two forms: a raw Base64 payload or a complete data URL. Normalize both forms before assigning src:
function toPdfDataUrl(value) {
if (typeof value !== 'string') {
throw new TypeError('Expected a string');
}
const trimmed = value.trim();
const prefix = 'data:application/pdf;base64,';
if (trimmed.startsWith(prefix)) {
return trimmed;
}
if (trimmed.startsWith('data:')) {
throw new Error('The data URL is not an application/pdf Base64 URL');
}
if (!trimmed) {
throw new Error('The Base64 payload is empty');
}
return prefix + trimmed;
}
const pdfUrl = toPdfDataUrl(apiValue);
document.querySelector('#preview').src = pdfUrl;
This avoids the common mistake of producing data:application/pdf;base64,data:application/pdf;base64,....
Iframe attributes and layout
title: describe the embedded document for assistive technology.widthandheight: set intentional dimensions. The default iframe size is only 300 by 150 CSS pixels, which is usually too small for a document preview. See the MDN iframe reference.- Responsive sizing: use
width: 100%and a CSS height such as70vhwith a sensible minimum. loading="lazy": useful when previews are below the fold, but omit it when the PDF must be immediately available.name: optional; use it only when other links or scripts need to target the frame.
Keep the download link outside the iframe. A native PDF viewer may fail because of browser policy, content security policy, an unsupported environment, or a malformed payload, and iframe child markup is not a reliable fallback.
When Base64 data URLs are a poor fit
Data URLs are intended for short inline values. A Base64 representation is larger than the original binary and places the entire document in the HTML or DOM attribute. Large values can increase memory use and make page rendering and navigation more expensive. RFC 2397 cautions that large data URLs are often inappropriate.
MDN documents current browser limits of 512 MB for Chromium and Firefox and 2,048 MB for Safari/WebKit, while noting that browsers are not required to support a particular maximum. These are upper limits, not practical PDF-size targets: framework limits, memory pressure, CSP, and mobile devices can fail much earlier. For large or frequently reused PDFs, serve a normal HTTPS PDF URL instead.
| Approach | Best fit | Tradeoff |
|---|---|---|
| Base64 data URL in native iframe | Small PDF already held as Base64; simplest preview | Large inline value, memory overhead, browser and policy variability |
| Hosted PDF URL in native iframe | PDF is already served from a URL | Requires delivery, access control, and cross-origin configuration |
| PDF.js with decoded bytes | Custom controls or programmatic PDF handling | Adds viewer integration; cross-origin retrieval needs CORS or a proxy |
For PDF.js, the project’s FAQ recommends opening raw bytes as a Uint8Array when possible because converting through Base64 uses more memory. A PDF fetched from another origin needs an appropriate CORS policy or a server-side proxy.
Security and policy considerations
Content Security Policy
Your page’s Content Security Policy can restrict iframe sources. Review the effective frame-src policy in the browser developer tools and adjust it only when your deployment permits data URLs. The MDN CSP guide explains the relevant directives.
Sandboxing
Do not add sandbox while diagnosing a blank native PDF preview. MDN warns that sandboxing can prevent the built-in PDF viewer from loading and is not a portable way to restrict native PDF previews. The browser’s PDF viewer already sandboxes executable PDF content.
Untrusted documents
Treat uploaded PDFs as untrusted input. Validate size and type on the server, authenticate access to sensitive documents, and avoid reflecting arbitrary user-controlled strings into HTML. A Base64 data URL is an encoding method, not an access-control mechanism.
Server-side Base64 examples
Python
import base64
from pathlib import Path
pdf_bytes = Path('document.pdf').read_bytes()
payload = base64.b64encode(pdf_bytes).decode('ascii')
data_url = f'data:application/pdf;base64,{payload}'
html = f'''<iframe src="{data_url}" width="100%" height="600" title="PDF preview"></iframe>
<a href="{data_url}" download="document.pdf">Download the PDF</a>'''
print(html)
Node.js
import { readFile } from 'node:fs/promises';
const bytes = await readFile('document.pdf');
const payload = bytes.toString('base64');
const dataUrl = `data:application/pdf;base64,${payload}`;
const html = `<iframe src="${dataUrl}" width="100%" height="600" title="PDF preview"></iframe>
<a href="${dataUrl}" download="document.pdf">Download the PDF</a>`;
console.log(html);
When generating HTML on the server, escape values correctly for the context in which they are inserted. For recurring or large documents, return a short-lived hosted URL instead of placing the complete Base64 value in every page response.
Troubleshooting a blank or broken iframe
- Wrong prefix: Confirm the value starts exactly with
data:application/pdf;base64,. There must be one comma and one prefix. - Invalid payload: Ensure the string is Base64 for PDF bytes, without JSON quotes, URL encoding, a filename, or accidental whitespace inserted by a transport layer.
- Duplicate data URL: If the input already starts with
data:, do not prepend another prefix. - Missing dimensions: Set an explicit height. The default 150-pixel iframe can look blank even when the viewer loaded.
- CSP rejection: Inspect the console for a blocked frame and check
frame-srcin the effective CSP. - Sandbox interference: Remove
sandboxduring diagnosis; it can stop the native viewer from loading. - Browser or embedded-webview limitation: Open the external download link. If it works there but not in the iframe, the payload is probably valid and the embedded viewer is the limitation.
- Document too large: Switch to a hosted URL or a PDF.js integration that consumes decoded bytes.
- Cross-origin fetch failure: If PDF.js or JavaScript fetches the PDF from another origin, configure CORS or proxy the request through your server.
Performance, reliability, and cost decisions
- Base64 increases representation size and duplicates the document when used both in an iframe and a download link.
- Keep previews lazy when a page contains many documents, and avoid embedding the same large payload multiple times.
- Use a hosted URL with caching and range support when users revisit documents or when files are large.
- For sensitive files, prefer short-lived authenticated URLs or a server proxy over exposing a permanent data URL in page source.
- A separate link gives users a recovery path when the native viewer is unavailable.
Or skip the browser setup
If your real goal is to capture a webpage as an image or PDF, ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns a clean PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can every browser render a PDF iframe?
No. The browser’s built-in PDF viewer and the surrounding application determine whether the preview works. Always provide an external link.
Should I URL-encode the Base64 payload?
Use the data URL syntax as shown. Do not replace the required comma or add a second data URL prefix. If a transport layer changes the string, normalize and validate it before assignment.
Can I use an object URL instead?
Yes. When you have a Blob, URL.createObjectURL(blob) avoids putting the entire Base64 string in HTML and is often a better browser-side choice for larger files. Revoke it with URL.revokeObjectURL when it is no longer needed.
Is a data URL suitable for permanent document storage?
No. It is an inline delivery format. Store the PDF as bytes and generate an authorized URL or stream it when documents are large, private, or reused.


