HTML to PDF

Paste HTML or drop an .html file, preview it safely, and download it as a PDF.

Drop an .html or .htm file here, or click to browse
Preview — sandboxed, 523 pt column

About HTML to PDF

Paste markup into the editor or drop an .html file onto the strip above. The document is cleaned, rendered in a sandboxed frame at exactly the column width the PDF will use, and turned into a file when you press download. The output holds real text rather than an image of a page, so it can be selected, searched and copied — and none of it involves a server.

What happens to the markup first

The HTML is parsed into an inert document — one with no browsing context, so nothing executes and no image is fetched while it is being examined. Script elements, iframes, frames, objects, embeds, applets, noscript blocks, base and meta tags and links to external stylesheets are deleted. Every element is then walked and stripped of inline event handlers, of srcdoc, ping and formaction attributes, and of any href or src whose value resolves to a javascript:, vbscript: or data:text/html URL, with whitespace and control characters normalised out first so that the classic evasions do not slip through. A srcset is a comma-separated list rather than one URL, so it is split and each candidate checked on its own — one dangerous entry costs only itself, not the whole list. The walk goes inside <template> content as well, which lives in a separate document fragment that an ordinary query would miss. The counter above the editor reports exactly how much was taken out. Only then does the markup reach the page, and even then the preview lives in an iframe with an empty sandbox attribute, which switches off scripting, forms, navigation and same-origin access.

How your CSS is treated

Style blocks are lifted out of the document and handed to the browser’s own CSS parser, then rewritten so that every rule sits under the page container: a rule targeting body, html or :root becomes a rule for the container itself, and every other selector is nested beneath it. A selector list is split only on commas that really separate selectors, so a[title="x,y"] and div:has(> a, > b) keep working. A rule nested inside another with native CSS nesting resolves against its enclosing selector to any depth, whether or not it uses the & placeholder. Media queries, @supports and @container blocks keep their conditions and have their contents scoped the same way; @layer blocks are flattened, because a cascade layer re-emitted here would rank your rules below this tool’s own defaults rather than above them. @keyframes are renamed with a prefix — together with every animation-name that refers to them, in a style attribute as much as in a rule — so a pasted animation cannot take over one of this page’s own. That rewriting is why a pasted stylesheet cannot restyle this tool, and why the same stylesheet can be used for both the preview and the PDF. The preview is assembled as a single HTML document, so the one character sequence that could end its stylesheet early — a </style> hidden inside a CSS string or a url(), which turned the rest of your rules into markup in the frame and fetched whatever that markup pointed at — is written back as the CSS escape that means the same thing to the parser and nothing to the HTML around it. Three kinds of rule are dropped: @import and @font-face, which would both fetch a remote file, and @page, which is superseded by the page size chosen here. Stylesheets loaded from a separate file are gone with the link tag, so inline your CSS before pasting. One thing a media query cannot do here is switch on print: the page is captured as it looks on screen, so @media print never matches.

What converts well, and what does not

  • Reliable: headings, paragraphs, lists, tables with borders and background fills, blockquotes, inline emphasis, text colour, spacing and simple block layout.
  • Usually fine: flexbox and grid, since the browser lays them out before anything is drawn.
  • Poorly: fixed and sticky positioning, box shadows, multi-column text, and anything that depends on scrolling or hover.
  • Removed, and removed from the preview too: CSS transforms and url() image references. The renderer underneath cannot carry either into a PDF — a rotated element used to disappear from the file and pad it out with a hundred blank pages, and an image named in CSS was fetched from its host and then drawn nowhere. Transforms go from ordinary rules, from style attributes and from inside @keyframes, where one reached the renderer through the running animation and cost a whole three-line document sixty-two pages with the animated line on none of them. url() goes from background-image, list-style-image, border-image, mask-image, shape-outside, clip-path, filter, cursor and generated content. Both are taken out before anything is rendered and reported above the editor, so the frame keeps telling the truth. Use an <img> for a picture that has to print.
  • Held still: CSS animations. A PDF is one frame, so every animation is frozen at its final keyframe — the state a fade-in or a slide-in was written to end at — in the preview and in the file alike. Without that the file caught whichever instant the renderer happened to reach, so two exports of the same markup came out different and neither matched the frame.
  • Collapsed: a <details> without the open attribute prints as its summary line only, the same as the preview and the same as your browser’s print output.
  • Absent: anything a script would have created, since no script runs. Copy the rendered DOM out of your browser’s inspector and paste that instead.
  • Styled but inert: links. An anchor keeps its colour and underline, but the PDF holds text rather than link annotations, so it is not clickable and the address is not stored in the file.

Fonts, images and pagination

Text is drawn using the standard PDF fonts, so your font-family declarations are mapped onto the nearest of a Helvetica-like sans, a Times-like serif and a Courier-like monospace — and the preview is switched to the same substitute, because the two have to be measured with the same metrics or every word boundary opens a gap. Only the WinAnsi character set has glyphs: Latin-1 plus curly quotes, dashes, bullet, ellipsis and the euro sign. Anything else — Cyrillic, Greek, CJK, emoji — is written as the wrong letter rather than left out, so the count of such characters is shown above the editor before you download; it is the one thing the frame cannot warn you about on its own, because your browser has the fonts and the file will not. Images referenced over http or https are embedded when the host allows cross-origin reads; base64 data URIs in an <img> always work; SVG — inline or referenced — is converted to a bitmap first, because the PDF writer here has no vector import. Text and pictures alike are flate-compressed, losslessly: the pixels in the file are the ones your browser drew, and a page with a logo on it comes out a few tens of kilobytes rather than the best part of a megabyte. The rendered column is sliced into pages at boundaries that never cut a line of text in half — a line that would straddle the boundary is moved whole to the next page — breaks cannot be forced at a chosen point, and content wider than the column is clipped near the right-hand page edge; switch the page size, or narrow the markup, if something is running off it.

Related tools

If the source is Markdown rather than HTML, Markdown to PDF takes the same route with a Markdown editor in front of it. For typed text with no markup at all, Create PDF is faster and produces a much smaller file. HTML Formatter is useful for tidying messy markup before pasting it here, and CSV to PDF is the better choice for a wide data table. Everything on this page runs in your browser: the markup you paste is never transmitted, which is the reason to convert a document containing customer details here rather than through a hosted converter.

Sanitised before it is shown

The markup is parsed into an inert document where scripts, frames, objects, external stylesheet links and every inline event handler are deleted, along with javascript: URLs — inside <template> content as well as in the visible tree. Only then is it rendered, and the preview itself sits in a sandboxed frame with scripting switched off.

Your CSS, scoped to the page

Style blocks in the pasted document are parsed by the browser and rewritten so every rule applies inside the page container: rules written for body or html target the container itself, everything else is nested under it, cascade layers are flattened so your rules still beat this tool’s defaults, and animation names are renamed so they cannot collide. Your layout survives; nothing leaks out into this tool, and nothing in it can end the stylesheet early to smuggle markup into the frame.

The frame is the page

The preview is drawn at exactly the column width the PDF uses, from the same sanitised markup, the same scoped stylesheet, the same substituted fonts and the same page colour — so what you see is what gets printed, with real selectable text rather than a screenshot. Anything the PDF cannot hold, or cannot spell, is taken out of the preview too or counted above the editor.

Frequently Asked Questions

What is removed from my HTML, and why?

Script elements, iframes, frames, objects, embeds, applets, noscript blocks, base and meta tags and external stylesheet links are deleted outright; so is every inline event handler attribute such as onclick or onerror, and any href, src or srcset pointing at (or containing, for the comma-separated list srcset accepts) a javascript:, vbscript: or data:text/html URL. The same pass runs inside <template> content, which is a separate document fragment that a plain query would skip. A counter above the editor tells you how many of each went. Two reasons: a PDF is a static document so none of it could run there anyway, and this page has to insert your markup into a real browser document to measure it, which would otherwise be a way to run code in the tool.

Are my style rules applied?

Yes. Style blocks are read out of the document, parsed by the browser’s own CSS engine and rewritten so that every selector applies inside the page container — a rule written for body or html becomes a rule for the container, and everything else is nested beneath it. That includes a background colour set on body: it paints the content column in the file exactly as it does in the frame. Selectors keep any comma that sits inside an attribute value or inside :is(), :where() or :has(). Native CSS nesting works too — a selector written inside another rule, with or without an & placeholder, resolves against its enclosing rule the same way it does in your browser, to any depth. Inline style attributes are kept, apart from the same properties listed below. Kept with their conditions: @media, @supports and @container. Flattened: @layer, whose only job is to reorder the cascade — re-emitting it would push your rules underneath this tool’s own defaults, so its contents are used directly instead. Dropped: @import and @font-face, because both fetch a remote file, and @page, because the page box is set by the size selector here. Rewritten: font-family (mapped onto the standard PDF families), @keyframes names (prefixed so they cannot collide with this page’s own animations, and the animation-name that refers to them is renamed with them, in a style attribute as well as in a rule), CSS transforms, and url() image references. The last two are removed wherever they appear — in an ordinary rule, in a style attribute, and inside a @keyframes block — because the PDF cannot hold either, and leaving them in the preview alone would make the frame lie; both are counted above the editor when they occur. Two caveats. A rule inside @media print never matches, because the page is drawn the way it looks on screen — move anything you need into an unconditional rule. And an animation is held at its final keyframe rather than played, in the frame and in the file alike, so that the same markup always converts to the same document.

My page loads its CSS from a separate file. Will that work?

No — a link to an external stylesheet is removed, so the document renders unstyled. Inline the CSS into a style block in the markup you paste and it will be applied in full. The same goes for anything else the page would normally fetch: an external script or a web font. The one remote fetch that can still happen is an <img> with an http address, which your browser loads for the preview. A pasted stylesheet does not reach out on its own: url() is taken out of every property that could ask for a file — background-image, list-style-image, border-image, mask-image, shape-outside, clip-path, filter, cursor and the content of a ::before or ::after — because none of them can be drawn into the PDF anyway, so the request would have bought you nothing but a line in that host’s log.

What about JavaScript that builds the page?

It never runs, so anything it would have created is absent. A single-page app whose body is an empty div converts to an empty PDF. The fix is to let the page render in your browser first, then copy the resulting DOM — right-click, Inspect, copy the outer HTML of the element you want — and paste that here.

Do images come through?

Images with an http or https address are fetched by your browser for the preview and embedded in the PDF, provided the host allows cross-origin reads; when it does not, the image is skipped and everything else still converts. Base64 data URIs in an <img> work everywhere and are the reliable option. Vector graphics are handled too: an inline <svg> and an SVG referenced by an <img> are both converted to a bitmap at twice their layout size just before the file is built, because the PDF format used here cannot take SVG directly — so they appear, but they are pixels rather than curves. Every picture is stored flate-compressed and lossless: the pixels are exactly the ones your browser drew, and a logo that would otherwise have added most of a megabyte to the file adds about ten kilobytes. A large photograph is still large — compression that keeps every pixel can only do so much — so scale a photo down before pasting it if file size matters. Note that loading a remote image is a request from your machine to that server — none of your markup is sent, but the host does see the request.

Which fonts does the PDF use?

The standard PDF font set: a Helvetica-like sans, a Times-like serif and a Courier-like monospace, each with bold and italic. No font file is embedded, so your font-family declarations are mapped onto the nearest of those — and the preview is switched to the same substitute, because leaving it in the original face would show word spacing the file cannot reproduce. A page set in a licensed brand font therefore will not look identical here or in the output. The encoding is WinAnsi, which covers Latin-1 plus the curly quotes, en and em dashes, bullet, ellipsis and euro sign; Cyrillic, Greek, Arabic, Hebrew, Devanagari, CJK and emoji have no glyph. Those characters are not left blank — the writer emits whatever byte it can, so they come out as unrelated Latin letters that still look like text. Because the frame uses your system fonts it shows them correctly and the file does not, which is the one place the preview cannot be trusted, so any such character is counted in a red strip above the editor before you download — including one written into a stylesheet as generated content, such as a custom bullet or icon set via the content property on a ::before or ::after, which prints exactly as if it were typed into the document.

How faithful is the layout, and how are page breaks chosen?

Straightforward document markup converts well: headings, paragraphs, lists, tables, blockquotes, inline styling, colours, borders and background fills. Complex layout is where it strains — flexbox and grid are laid out by the browser and usually survive, but position: fixed, sticky headers, box shadows and multi-column layouts translate poorly or not at all, and CSS transforms are removed before rendering — including one written inside a @keyframes block, which used to reach the renderer through the running animation and take its whole element out of the file. An element positioned fixed is drawn against the window rather than the page and usually falls off it entirely. A closed <details> prints as its summary line only, the way your browser prints it; add the open attribute to include the body. The rendered column is sliced into pages at boundaries that never cut a line of text in half: a line that will not fit above the bottom margin is moved down to the top of the next page in one piece. That move used to lose the line outright at roughly one page break in three — a six-hundred paragraph document arrived five paragraphs short — which is fixed, and long documents are now checked row by row. You cannot force a break at a chosen place, anything wider than the column is clipped at the right edge, and a single line of text taller than the printable band of the page (a display heading of several hundred points, say) still cannot be placed and will be missing.

Which files can I load, and is anything uploaded?

The drop zone takes .html and .htm files, read as UTF-8 text, and pasting into the editor works just as well; anything else is refused with a message rather than ignored. Nothing is uploaded: parsing, sanitising, previewing and PDF generation all happen inside this browser tab, and the file is saved straight to your downloads folder. It is named after the file you loaded, or page.pdf when you typed the markup yourself, when the file you loaded was empty, or once you have emptied the editor.