Creating PDFs from HTML + CSS in JavaScript: What actually works
Developers often assume that generating a PDF from styled HTML is as simple as passing a DOM node to a library and hitting “download.” In reality, converting the dynamic, flowing nature of HTML and CSS into a static, print-ready PDF—all in the browser—comes with unexpected challenges.
From missing CSS features to blurry image rendering and broken page breaks, browser-based HTML-to-PDF conversion is a minefield of workarounds and limitations. This article explores what actually works, which libraries are worth your time, and how to approach client-side PDF generation with realistic expectations.
Why Generating PDFs from HTML Is a Developer Headache
On the surface, it sounds simple: take the styled HTML your app already renders and save it as a PDF. But the moment developers try to do this client-side, reality hits hard.
PDFs and HTML were never meant to speak the same language. HTML is flexible, dynamic, and flows to fit different screens and interactions. PDFs, on the other hand, are static, fixed-layout documents meant for precise print output. The conversion between the two isn’t just a technical challenge—it’s a conceptual mismatch.
What complicates this further is that the tools available for browser-based PDF generation are limited in how they interpret and render CSS. Many modern layout features—like Flexbox, Grid, and media queries—are poorly supported or ignored altogether. Font handling, background images, page breaks, and z-index stacking are also common points of failure.
And while server-side solutions like Puppeteer can generate near-perfect replicas by rendering a real browser environment, they come with infrastructure costs and can’t run purely client-side. For developers looking to stay serverless, lightweight, and private, that option is often off the table.
The end result? You might spend more time fighting CSS quirks and rendering bugs than actually generating documents.
What Makes HTML-to-PDF So Hard?
Turning HTML into a PDF may seem like a basic export task—but under the hood, it’s one of the most technically frustrating things a frontend developer can attempt.
Here’s why:
1. HTML and PDF Use Different Layout Models
HTML is fluid. It reflows content based on screen size, font metrics, and responsive breakpoints. PDFs are rigid. Every element must be positioned precisely on a fixed-size canvas.
That means features we rely on—like width: 100%
, display: flex
, or media queries—don’t naturally translate. PDF engines require hard numbers: exact X/Y positions, fixed widths, and explicit page breaks.
2. CSS Rendering Support Is Incomplete
Most client-side HTML-to-PDF tools don’t run a full browser engine. Instead, they simulate the DOM and paint elements manually onto a canvas or PDF buffer.
The catch? They only support a subset of CSS:
Complex layouts (e.g. CSS Grid,
position: sticky
) are unreliable.Pseudo-elements (
::before
,::after
) often don’t render.@media print
styles are usually ignored.External stylesheets or dynamically injected styles may be skipped unless carefully inlined.
3. Text and Fonts Get Lost in Translation
Some tools rasterize HTML into an image before embedding it in a PDF. That’s fast and visually consistent—but comes at a cost:
Text becomes non-selectable and non-searchable.
Font fidelity may suffer.
File sizes can balloon due to high-res image embedding.
Other tools preserve vector text, but require you to manually embed fonts or convert styling into low-level primitives.
4. Page Breaks and Multi-Page Layouts Are Painful
The browser scrolls infinitely. PDFs don’t.
When converting HTML content into multi-page PDFs, you often need to:
Calculate when a section should break
Prevent elements from splitting across pages
Dynamically insert headers and footers
Even tools that offer page break helpers struggle when layout becomes dynamic or deeply nested.
The short version? HTML and PDF speak different dialects, and getting them to communicate smoothly—especially in a browser—is far more nuanced than it first appears.
The Client-Side Toolkit: What’s Actually Available?
If you’ve ever typed “HTML to PDF JavaScript” into a search engine, you know the sea of libraries can be overwhelming. But in reality, only a few libraries truly generate PDFs in the browser—and most rely on hidden helpers behind the scenes.
When you look closely, the client-side ecosystem breaks into two groups:
Primary PDF Libraries: These handle end-to-end HTML-to-PDF conversion (or offer tools to help you build PDFs programmatically).
Supporting Utilities: These convert DOM elements into images or canvas elements—but cannot generate PDFs by themselves.
Let’s break them down.
Primary PDF Libraries
These are the libraries you’ll actually use to create PDFs in the browser.
html2pdf.js
(wraps html2canvas
+ jsPDF
)
Type: Image-based PDF generator
How it works: Takes a screenshot of an HTML element (
html2canvas
) and embeds it into a PDF (jsPDF
)Pros:
Easy to use, minimal setup
Great for simple, static content like certificates
Fully client-side
Cons:
Renders content as an image (text is not selectable)
Limited support for CSS (especially modern layouts)
Fonts may look blurry at print resolution
jsPDF
(with .html()
method)
Type: DOM-based capture (with fallback to
html2canvas
)How it works: Attempts to parse the DOM and build a PDF, but relies on
html2canvas
for styled contentPros:
Lightweight, programmatic control
Works for basic HTML with inline styles
Cons:
Poor support for external stylesheets
Output is often distorted or oversized
Difficult to use for real-world styling
pdfmake
(with html-to-pdfmake
helper)
Type: Structured, declarative PDF generator
How it works: Uses a JSON schema to define layout;
html-to-pdfmake
helps convert basic HTML into this formatPros:
Outputs real, selectable text (vector rendering)
Strong support for tables, layout, multi-page content
Great for invoices, reports, form-driven documents
Cons:
Steeper learning curve
Limited HTML and CSS compatibility
Styling done via config, not DOM or CSS
Supporting Utilities (Not PDF Generators)
These tools help render HTML to canvas or image formats, but do not generate PDFs on their own.
html2canvas
Type: DOM ➝ Canvas renderer
How it works: Converts a DOM element into a
<canvas>
image (used internally byhtml2pdf.js
and others)Pros:
Easy to use for screenshot capture
Works with most basic HTML/CSS
Cons:
Incomplete CSS support (e.g., Grid/Flex)
Cannot generate PDFs directly
CORS issues with external images
dom-to-image
Type: DOM ➝ Image (PNG, SVG)
How it works: Converts a DOM node to a downloadable image
Pros:
Good for image previews or animations
Supports SVG output for crisp visuals
Cons:
Doesn’t support PDF export
Suffers similar limitations as
html2canvas
with styling
TL;DR: Two Camps, Two Trade-Offs
Tool | Type | Output Format | Selectable Text | Best Use Case |
---|---|---|---|---|
| PDF Library | Image in PDF | ❌ | Quick, styled exports |
| PDF Library | Image/HTML | ❌ | Basic DOM snapshots |
| PDF Library | Vector PDF | ✅ | Structured documents |
| Utility (screenshot) | Canvas/Image | ❌ | Pre-render content for PDF |
| Utility (screenshot) | Image (PNG/SVG) | ❌ | Export styled visuals as images |
But before diving into styling challenges, let’s take a closer look at the rendering strategies themselves—because how content is rendered shapes everything else.
Vector vs. Raster Rendering: Why It Matters
Not all HTML-to-PDF tools are created equal—and the difference often boils down to how they render content: as vectors or raster images.
Raster-Based Rendering
Libraries like html2canvas
(and tools built on it, such as html2pdf.js
) take a screenshot-style approach. They render your HTML as a bitmap image, then embed that image inside a PDF. Think of it like printing a web page and scanning it.
Pros:
Relatively easy to implement
Faithful visual capture of complex layouts (what you see is what you get)
Cons:
Text is not selectable or searchable
Quality suffers when zoomed or printed (blurry or pixelated)
File sizes can be large, especially with images or long pages
Vector-Based Rendering
Libraries like pdfmake
use vector instructions to construct each part of the PDF—from text to shapes to tables. You’re not capturing pixels—you’re describing structure.
Pros:
Text is crisp, searchable, and scalable
Smaller file sizes
Output quality is more professional and print-ready
Cons:
Requires a more abstract, programmatic setup
Doesn’t directly mirror your existing HTML/CSS—layouts must be rebuilt using JavaScript objects
Choosing between these two rendering models isn’t just a technical detail—it directly affects the usability and quality of your PDF output. If you need visual fidelity, raster might suffice. If you need searchability and print precision, go vector.
In the next section, we’ll break down how CSS plays into this—and why styling is often where things fall apart.
CSS Support and Styling Constraints
Even the most promising HTML-to-PDF tools can falter when faced with real-world CSS. Why? Because browsers and PDF engines speak very different layout languages—and not all styling features translate cleanly.
Here’s what developers typically run into:
Limited CSS Feature Support
Modern Layouts: Flexbox and Grid often render inconsistently—or not at all—especially in raster-based tools like
html2canvas
. In structure-based tools likepdfmake
, layout must be recreated using its custom schema.External Stylesheets: Tools like
jsPDF().html()
frequently ignore external stylesheets—unless those styles are inline or already computed in the DOM. This often leads to layout shifts or broken formatting.Media Queries: Libraries don’t honor
@media print
rules, meaning dark mode or responsive styles might sneak into exported PDFs unintentionally.Pseudo-elements and Transforms:
::before
,::after
,transform
, andz-index
often don’t render—or render incorrectly—resulting in misplaced or missing content.
Styling Hacks Are Often Required
To work around these issues, developers often need to:
Inline all critical styles and remove reliance on external CSS
Manually restructure layouts that previously relied on flex or grid
Create dedicated “print views” stripped of animations, interactive elements, or dark mode themes
Resize and reflow content to fit standard PDF dimensions and prevent overflow or cut-off sections
These band-aids help—but they result in duplicated styling logic and make maintenance harder.
When Rendering Tools Don’t Think Like Browsers
We found that jsPDF().html()
—despite promising vector-based, searchable text—can render content at incorrect scales, especially when layout widths exceed default PDF page sizes. In our tests, even a modest table layout spilled into multiple pages unless we manually tweaked dimensions or switched to landscape mode.
Meanwhile, pdfmake
's helper (html-to-pdfmake
) preserves structure, but strips most CSS nuance. Alignment, spacing, and styling fidelity often degrade unless redefined using its custom syntax.
Image-Based Rendering Has Its Own Drawbacks
For tools like html2canvas
or html2pdf.js
, the rendering process creates an image—not actual DOM content. That introduces:
Blurry fonts or jagged text, especially on high-DPI screens
Non-selectable and non-searchable output
No real layout intelligence—just a screenshot
Font fallbacks if external fonts aren’t accessible or embedded
These tools work best for simple, single-page documents where appearance matters more than precision.
In the next section, we’ll look at how these limitations play out in real examples—so you can see what actually works, what breaks, and what’s worth fixing.
Real-World Examples: What Works (and What Doesn’t)
So how do these tools behave when tested in the wild? Below are simplified examples that reflect common developer use cases—along with what actually happens when you run the code in a real browser.
Example 1: html2pdf.js
for a Simple Certificate
What works:
Very fast to implement—just point it at an HTML node
Preserves most on-screen styling, including fonts, colors, and layout
Great for simple, static exports like certificates, badges, or receipts
What breaks down:
Text is not selectable or searchable—it’s just a screenshot
Output can appear blurry on high-DPI or Retina screens
Doesn’t handle long or paginated content well—everything gets flattened into a single image, even if it spans multiple pages
Example 2: jsPDF().html()
with Styled Content
What works:
Uses the actual DOM to generate content—no canvas screenshot required
Text remains searchable and selectable in the PDF (vector-based output)
Can handle basic inline styles (colors, fonts, spacing)
Runs entirely in the browser—no server dependency
Note: jsPDF produces vector-based output when using .html() directly. However, if paired with html2canvas (as some older examples do), the rendering becomes rasterized and text is no longer selectable.
What breaks down:
External stylesheets are ignored—only inline or computed styles are applied
Layout may break or overflow, especially with fixed-width containers (e.g., 800px tables in portrait mode)
Responsive designs don’t translate well—PDFs have fixed dimensions, but HTML layouts often assume fluid widths
Layout quirks include unexpected zoom, oversized elements, or extra pages—especially if widths and padding aren’t tightly constrained
No built-in support for page breaks—content that overflows often gets cut off without warning
In our testing, even a modest invoice-style layout ended up overflowing the page horizontally in portrait mode. While switching to landscape orientation can fix this, it highlights a deeper issue: jsPDF().html()
doesn’t “understand” layout context the way a browser does.
This tool can be useful—but it requires careful content sizing and style isolation to avoid unexpected breakage. It’s not a plug-and-play option for fully styled web pages.
Example 3: pdfmake
with html-to-pdfmake
Helper
What works:
Produces real vector-based PDFs with selectable, searchable text
Handles tables and structured layouts with clarity and precision
Maintains semantic structure better than image-based tools
What breaks down:
Ignores most CSS styling beyond color, font weight, and basic text properties
Alignment styles (like
text-align: center
orright
) often fail to translate—and may default to left alignment in the outputMore complex layouts typically need to be rebuilt using
pdfmake
’s schema for accurate results
Takeaway
Each tool has practical value—but only within its sweet spot:
For static layouts with minimal styling, image-based tools like
html2pdf.js
offer a quick win.For searchable text and structured layouts, libraries like
pdfmake
—with helper utilities—are more reliable.But if your needs go deeper, consider hybrid workflows or server-side rendering (covered in a later section).
In the next section, we’ll look at browser and mobile quirks that can impact even well-crafted solutions.
Browser Limitations and Mobile Quirks
Even if your HTML-to-PDF setup works perfectly on desktop, browser-specific quirks—especially on mobile—can quickly derail your output. These aren’t just edge cases; they’re common gotchas that affect real users—and we ran into several of them during testing.
Canvas Size Limits (iOS Safari Especially)
If you're using tools like html2canvas
or html2pdf.js
, you're relying on the browser’s ability to render a full document as a <canvas>
. But mobile browsers, especially iOS Safari, impose hard limits on:
Canvas height or pixel area: If your content exceeds the limit, the canvas (and therefore your PDF) may render blank or partially cut off.
Memory usage: Large canvas operations can trigger performance throttling or tab crashes—especially on older phones or tablets.
Workaround: Split large documents into smaller chunks and paginate manually, or avoid canvas-based tools altogether for long reports.
Viewport Assumptions and DOM Rendering Quirks
Libraries like jsPDF().html()
rely on computed layout dimensions—but they don’t always interpret the DOM the way a browser does. During our testing, we saw:
Overflows and cut-off elements when fixed-width content (like 800px tables) didn’t fit inside default portrait dimensions.
Zoomed or scaled-up rendering caused by poor handling of parent containers, body margins, or default zoom levels.
Layout shifts between browsers due to font fallback, differing box models, or unsupported inline styles.
Tip: Manually resize or constrain layout widths before export, and test across multiple viewports (portrait, landscape) to reduce surprises.
Lazy Loading, CORS, and Missing Images
Many modern sites use lazy-loaded images (loading="lazy"
) or fetch assets from CDNs. These features don’t always play well with client-side PDF tools:
Lazy-loaded images may not load in time before the canvas or DOM snapshot occurs—leading to missing visuals.
Cross-origin (CORS) images can "taint" the canvas, preventing rendering or crashing the PDF step.
CSS-based images, masks, or background gradients are inconsistently supported, especially in raster-based tools.
Workaround: Preload images, disable lazy loading temporarily, and ensure proper CORS headers are set if fetching external assets.
Download Behavior Varies by Browser
Even once you’ve generated a PDF blob, getting it to download correctly isn’t guaranteed:
iOS Safari often refuses to download PDFs directly to the file system—it opens them in a new tab instead.
Pop-up blockers can interfere with
window.open()
or automatic triggers from JavaScript libraries.Print preview flows (like
pdfMake.print()
) behave inconsistently across platforms.
Tip: Always provide a visible fallback (e.g. a download button with target="_blank" or download attributes) for cross-browser stability.
Browser behavior isn’t always in your control—but understanding these quirks early on can save you hours of debugging. In the next section, we’ll zoom out to look at the bigger picture: when client-side isn’t the best tool for the job.
When to Use a Hybrid or Server-Side Approach
Client-side tools are impressive—but they can’t do everything. As your documents grow in complexity, length, or styling precision, it may be time to offload rendering to a server or headless browser.
Signs You’ve Hit the Client-Side Ceiling
You might want to consider a hybrid or server-side approach if you encounter:
Consistent rendering issues with complex layouts, even after workarounds
High-fidelity branding requirements (e.g., pixel-perfect PDF output)
Very large documents that crash the browser or render incomplete on mobile
Custom fonts and media-heavy content that bloats the bundle or causes CORS issues
Dynamic, paginated content (e.g., long tables or multi-page reports)
Client-side solutions can struggle under these demands—not because they’re bad, but because browsers aren’t PDF engines.
What Server-Side Rendering Unlocks
Tools like Puppeteer, Playwright, or commercial services (like DocRaptor or Cloudlayer) use a real browser engine to generate your PDF—just like printing a webpage:
100% CSS support, including
@media print
, Flexbox, Grid, and even animationsAccurate pagination across multi-page layouts
Embedded web fonts and high-resolution images
Improved accessibility and selectable/searchable text
Better internationalization (e.g., RTL or multi-language content)
You send HTML/CSS to the server, render it headlessly, and return the final PDF to the client. Yes, it adds backend complexity—but it drastically improves rendering fidelity and cross-browser reliability.
Hybrid Models: Best of Both Worlds
Don’t want a full backend stack just for PDFs? Try a hybrid model:
Render layout client-side → send HTML snapshot or sanitized DOM to a backend PDF microservice
Or use headless rendering only when needed (e.g., fallback for unsupported devices)
Many frontend teams now combine client-side speed with server-side precision—reserving the heavy lifting for special cases.
In short, don’t feel locked into one approach. The best PDF stack is the one that meets your users’ needs without adding unnecessary pain for your team.
Up next: when it's time to stop fiddling with low-level tools altogether—and lean on higher-level platforms like Joyfill.
When (and Why) to Use a High-Level Solution Like Joyfill
As you’ve seen, client-side tools for HTML-to-PDF can do a lot—but they all operate within the limits of the browser’s rendering capabilities. If you find yourself wrestling with CSS quirks, performance trade-offs, or multi-page layout hacks, it might be time to step back and ask:
Should I keep pushing these tools—or is there a better abstraction?
High-level PDF generation platforms (like Joyfill) rethink the problem entirely. Rather than converting rendered HTML into a static PDF, they start from structured data and business logic—then generate forms and PDFs from that source of truth.
This approach is particularly useful when your use case includes:
Dynamic, form-driven documents with logic (e.g., field visibility, validation)
JSON data structures that need to map cleanly into PDFs
Teams maintaining reusable, versioned document templates
SaaS applications embedding fillable forms or submission workflows
Cross-platform rendering consistency (desktop, mobile, web)
These platforms aren't just about fidelity—they're about maintainability, scale, and developer velocity. By separating layout, logic, and data, high-level tools let you ship complex PDF experiences faster—and with fewer moving parts to debug later.
In the final section, we’ll recap the landscape and help you decide what tool fits your team, your users, and your document stack.
Final Thoughts: Pick the Right Tool, Then Ship
Turning HTML and CSS into PDFs in the browser sounds simple—until you try it. What seems like a straightforward task quickly runs into the reality of rendering mismatches, styling inconsistencies, and platform quirks.
Raster-based tools like
html2pdf.js
are fast and convenient, but render your content as images—resulting in non-selectable text, blurry visuals, and poor scaling.DOM-based tools like
jsPDF().html()
offer vector output, but struggle with CSS fidelity, external stylesheets, and responsive layouts.Declarative tools like
pdfmake
prioritize structure and searchability, but force you to translate HTML into a custom object format.Hybrid or server-side solutions (e.g. Puppeteer) unlock full fidelity, but introduce infrastructure, security, and cost considerations.
There’s no silver bullet. But there is a best-fit solution—depending on your needs:
Need a quick, informal export? Raster tools are fine.
Need selectable text and layout precision? Go vector.
Need consistent, scalable forms inside a web app? Consider high-level platforms.
Then prototype. Test in your browser. Stress the limits. And when those limits start costing you time and complexity?
If you're building PDF experiences inside your own SaaS application, Joyfill makes it easy to go beyond the browser—offering a data-driven, form-aware platform for scalable PDF generation.
Pick your tools wisely. Then ship confidently.