How to Generate PDFs in React Native from HTML and CSS
Convert HTML and CSS into a PDF in React Native with Expo Print or a native library. Learn about page sizing, assets, platform differences, and sharing.
To generate a PDF from HTML and CSS in an Expo app, use expo-print and call Print.printToFileAsync({ html }). It writes a PDF into the app’s cache and returns a file URI and page count. You can share that URI with expo-sharing. For a native React Native integration, react-native-html-to-pdf provides a generatePDF method with output options. The rendered result can vary by platform, so verify page breaks, images, fonts, and margins on both iOS and Android.
This guide shows how to convert HTML to PDF in React Native, choose an implementation, and handle the file it creates. See the Expo Print documentation and the react-native-html-to-pdf README for current setup details.
1. Choose the PDF generation path
| Project or requirement | Starting point | What to account for |
|---|---|---|
| Expo app generating a PDF from HTML in the app | expo-print |
Works on iOS and Android. The PDF is in the app cache; save or share it if it needs to persist. Local asset URLs in HTML are unsupported by Expo’s iOS printing path. |
| Existing native React Native project needing an HTML-string API | react-native-html-to-pdf |
Review the library’s native setup and compatibility with your React Native versions. Its README documents file and page options, with some platform-specific behavior. |
| Android app that should open the system print workflow | Android HTML printing framework | This launches a print job through print services. Android’s documented HTML printing path does not support CSS print attributes such as landscape. |
For Expo, the shortest documented route is printToFileAsync. Use printAsync when the goal is to show the native print interface; it is not the same operation as generating a PDF file directly.
2. Generate and share a PDF with Expo
Install the Expo modules with Expo’s version-aware installer:
npx expo install expo-print expo-sharing
For an existing React Native project using Expo Print, the Expo documentation says the expo package should also be installed. Then create a complete HTML document and pass it to Print:
import * as Print from 'expo-print';
import * as Sharing from 'expo-sharing';
const html = `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body { font-family: Arial, sans-serif; color: #222; padding: 24px; }
h1 { font-size: 24px; }
.muted { color: #666; }
@media print {
.page-break { break-before: page; }
}
</style>
</head>
<body>
<h1>Monthly report</h1>
<p class="muted">Generated from HTML and CSS.</p>
<section class="page-break">
<h2>Details</h2>
<p>Put the document content here.</p>
</section>
</body>
</html>`;
export async function createAndSharePdf() {
const { uri, numberOfPages } = await Print.printToFileAsync({ html });
console.log(`Created ${numberOfPages} page(s): ${uri}`);
await Sharing.shareAsync(uri, {
UTI: '.pdf',
mimeType: 'application/pdf',
});
return { uri, numberOfPages };
}
The returned uri identifies a generated PDF in the app cache. Sharing can be offered immediately, as above. If the app needs a durable copy, copy or move the file to an appropriate persistent location with a file-management step; a cache location should not be treated as permanent storage.
Keep the HTML as a well-formed document beginning with <!DOCTYPE html>. This is especially relevant if enabling the iOS markup formatter with margins, because Expo warns malformed markup can leave a blank final page in that configuration.
3. Set page dimensions, margins, and platform options
printToFileAsync accepts an HTML string and page-related options. Expo documents defaults of 612 by 792 pixels, corresponding to US Letter at 72 PPI.
const result = await Print.printToFileAsync({
html,
width: 612,
height: 792,
margins: {
top: 36,
right: 36,
bottom: 36,
left: 36,
},
});
| Option | Use | Platform note |
|---|---|---|
html |
HTML document to render into a PDF. | Provide a complete document and inline assets when required. |
width, height |
Set the page dimensions in points/pixels as documented by Expo; the defaults are 612 by 792. | Validate the physical layout and pagination on each target platform. |
margins |
Set top, right, bottom, and left margins. | Expo documents this option for iOS. On Android, printed margins can depend on the WebView engine, and HTML @page styles may override them. |
textZoom |
Adjust text zoom when needed. | Android only. |
useMarkupFormatter |
Use the iOS markup formatter instead of the default WebView formatter. | iOS only; Expo says this formatter does not display images. |
base64 |
Request an optional base64 representation in the result. | Available only when requested; sharing the returned file URI is the ordinary path. |
CSS support is not guaranteed to be identical between iOS and Android. Android’s documented native HTML print workflow specifically does not support CSS print attributes such as landscape. For orientation and print-specific behavior, check the exact API route you use and inspect output on device rather than assuming browser CSS parity.
4. Include images, fonts, and other assets
Expo’s iOS HTML printing path cannot load local asset URLs because of WKWebView limitations. A path such as file:///... that works elsewhere may therefore leave an image missing in the PDF. Expo documents converting local images to base64 and inlining them into the HTML as a workaround:
// Conceptual pattern: obtain a base64 data URL using your app's asset/file APIs.
const imageDataUrl = `data:image/png;base64,${imageBase64}`;
const htmlWithImage = `
<!DOCTYPE html>
<html><body>
<img src="${imageDataUrl}" alt="Chart" />
</body></html>`;
const pdf = await Print.printToFileAsync({ html: htmlWithImage });
The snippet shows where the data URL goes; use the asset and file APIs appropriate to your project to obtain imageBase64. Base64 increases the size of the HTML string, so inline only assets needed in the output. The react-native-html-to-pdf README also exposes base64 output, but does not recommend it as the default way to transfer a generated PDF.
Fonts and CSS can also differ across native rendering engines. Use fonts available to the rendering environment or verify any custom font setup supported by your chosen library. The native library README documents Android custom-font configuration; follow its current setup instructions.
5. Use react-native-html-to-pdf in a native project
The library’s documented API takes an HTML string and options such as filename, output directory, page width and height, and optional base64 output. Its installation and native linking/build requirements depend on your project setup and versions, so follow the current maintainer README rather than assuming it can be dropped into every Expo configuration.
import RNHTMLtoPDF from 'react-native-html-to-pdf';
const html = `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>body { font-family: Arial, sans-serif; }</style>
</head>
<body><h1>Invoice</h1><p>Payment due: 30 days</p></body>
</html>`;
async function makePdf() {
const file = await RNHTMLtoPDF.generatePDF({
html,
fileName: 'invoice',
width: 612,
height: 792,
// directory and base64 are optional; see platform-specific notes below.
});
return file;
}
Check the returned fields in the installed version’s README and type definitions. The documented custom directory value on iOS is Documents; do not assume directory names or path controls behave identically on Android and iOS. The library’s optional base64 result is not the recommended default for moving large PDFs around.
6. Build HTML that paginates predictably
- Use a complete document with a doctype, head, viewport declaration, and body.
- Choose the page dimensions intentionally and leave enough printable space around content.
- Use print CSS for breaks sparingly, then inspect the result on both platforms; renderer support can vary.
- Keep tables and other wide content within the page width. Test long text, large images, and unusually small or large documents.
- Inline local images as base64 for Expo’s iOS route. Confirm network-hosted assets are reachable in the rendering environment rather than relying on a development-only URL.
- Check fonts, line wrapping, headers, footers, margins, and the final page for clipping or unexpected blank pages.
7. Troubleshoot common PDF generation problems
| Symptom | Likely cause | What to do |
|---|---|---|
| Local image is missing on iOS with Expo Print | WKWebView-based printing does not support local asset URLs. | Convert the image to a base64 data URL and inline it in the HTML. |
| PDF has a blank final page | Malformed HTML combined with the iOS markup formatter and margins can cause this. | Start with a valid document beginning with <!DOCTYPE html>; test without the formatter to isolate the cause. |
| Margins differ between devices | Android printed margins may depend on its WebView engine; CSS page rules may override settings. | Check the actual WebView/rendering version, inspect @page rules, and tune against output from each target platform. |
| Landscape CSS has no effect | Android’s documented HTML print workflow does not support CSS print attributes such as landscape. | Confirm which PDF API path is in use and whether its page dimensions or orientation controls meet the requirement. |
| PDF is created but cannot be found later | Expo writes the generated file to the app cache, which is not a durable destination. | Share it promptly or copy it into persistent app storage using a file-management step. |
| Output differs from a browser preview | Native platform renderers do not guarantee identical CSS behavior. | Reduce unsupported or complex layout dependencies and render representative documents on both iOS and Android. |
| Native library install or build fails | Native setup or package compatibility may not match the project’s React Native/Expo configuration. | Check the library’s current installation instructions and compatibility for the exact project versions; rebuild the native app after native dependency changes. |
8. Performance, reliability, and cost
On-device generation avoids sending the HTML document to a separate conversion service, but it still uses device CPU and memory to render the page and produce a file. Large HTML documents, high-resolution images, and base64 inlining increase the work and input size. Keep documents focused, resize images to the dimensions needed in print, and avoid embedding unnecessary assets.
For reliability, treat PDF creation as an asynchronous operation that can fail. Show a progress state, handle rejected promises, and make retry behavior explicit. Do not report success until a file URI is returned. If the document contains user data, consider the consequences of sharing or storing the resulting file and use the app’s normal access controls.
These libraries and native APIs do not require a per-document hosted conversion API in the flow shown here. The relevant cost is implementation and device resource use; a separate hosted service may have its own charges and data-handling tradeoffs, which should be checked before adoption.
9. Or skip the browser setup
If your workflow is to capture a rendered web page as a PDF, ScreenshotNeo can do it with one API request, without building a browser-rendering flow into your app. ScreenshotNeo is a website screenshot API and MCP server for developers by Yorker Media. 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 \
-d format=pdf \
-o page.pdf
Replace the example URL with the page to capture and use your API key. ScreenshotNeo accepts one GET request with a URL and can return a PDF or a PNG, JPEG, or WebP screenshot. It removes cookie/consent banners, newsletter popups, and chat widgets before capture; each of those cleanup steps can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
10. Validate the PDF before shipping
- Generate sample PDFs on every target platform and OS version you support.
- Include edge-case documents: a one-page file, a long multi-page file, a wide table, long unbroken text, and local images.
- Check actual page count, margins, font rendering, image loading, page breaks, and the final page.
- Test sharing and any persistence step separately from PDF generation.
- Repeat the checks after changing the native library, Expo SDK, WebView environment, or document CSS.
FAQ
Does Expo Print return a PDF file or open a print dialog?
printToFileAsync creates a PDF file and returns its URI and page count. printAsync opens the platform’s print flow.
Can I use HTML templates with React Native?
Yes. Pass an HTML string to Expo Print or the native library’s PDF generation API. React Native does not render the HTML as a native view in these examples; the PDF implementation uses platform rendering components.
Can Expo Print save directly to permanent storage?
The documented generated PDF location is the app cache. Add a file-management step if the file must remain available beyond the cache lifecycle.
Will the same CSS produce identical PDFs on iOS and Android?
Do not assume so. Rendering and margin behavior can differ, and the Android HTML print workflow has documented print-CSS limitations. Test the documents you ship on each platform.


