How to Convert HTML to PDF with Microsoft Playwright
Learn how to convert HTML to PDF in C# with Microsoft.Playwright, control print and screen CSS, fix layout issues, and automate reliable exports.
Use Page.PdfAsync after loading the page. In Microsoft.Playwright for .NET, this saves a PDF when you provide a path and returns the PDF data. Playwright uses print CSS by default. Select screen media first when the PDF should match the browser’s screen presentation.
await page.PdfAsync(new() { Path = "output.pdf" });
For a complete workflow, install the Microsoft.Playwright package, install the browser binary that matches your Playwright version, open a page, wait for the content and assets you need, choose print or screen media, and export.
1. Install Microsoft.Playwright and its browser
Add the package to your .NET project:
dotnet add package Microsoft.Playwright
Playwright versions require specific browser binaries. After installing or upgrading the package, run the Playwright install step again if the matching browser is not present. Microsoft’s browser guidance explains that each Playwright version needs specific browser binaries: Playwright browser installation.
playwright install chromium
If the command is not available directly in your shell, build the project first and run the generated Playwright script from the build output. In Linux CI environments, install the browser’s system dependencies as documented by Playwright:
playwright install --with-deps chromium
2. Minimal C# conversion
This complete example opens a URL and writes a PDF file:
using Microsoft.Playwright;
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new()
{
Headless = true
});
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com", new()
{
WaitUntil = WaitUntilState.NetworkIdle
});
await page.PdfAsync(new()
{
Path = "output.pdf"
});
Path saves the generated PDF. The API also returns the PDF buffer, which is useful when you need to upload the result or return it from an HTTP endpoint instead of writing it locally.
3. Convert an HTML string instead of a URL
Use SetContentAsync when your HTML is generated by your application:
using Microsoft.Playwright;
var html = """
Invoice
Generated from an HTML string.
""";
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var page = await browser.NewPageAsync();
await page.SetContentAsync(html, new()
{
WaitUntil = WaitUntilState.NetworkIdle
});
await page.PdfAsync(new() { Path = "invoice.pdf" });
When the HTML references external fonts, images, stylesheets, or scripts, make sure those resources are reachable from the browser context. For deterministic builds, serve assets from stable URLs or embed the required CSS and images.
4. Print CSS versus screen CSS
PDF generation uses print media by default. That means @media print rules apply and screen-only layouts may change. Leave the default when the document has deliberate print styling.
await page.PdfAsync(new() { Path = "print-styled.pdf" });
To export the screen presentation instead, select screen media before calling PdfAsync:
await page.EmulateMediaAsync(new()
{
Media = Media.Screen
});
await page.PdfAsync(new() { Path = "screen-styled.pdf" });
Choose based on the intended output:
| Goal | Media setting |
|---|---|
| Use print-specific rules and page layout | Default print media |
| Match the screen design | Media.Screen |
5. Page size, margins, scale, and backgrounds
The Page PDF API supports paper formats and dimensions, margins, page ranges, scaling, background printing, and CSS page-size precedence. The exact .NET property names depend on the Microsoft.Playwright version, so check the Microsoft.Playwright .NET Page API for the version installed in your project.
A typical configuration looks like this:
await page.PdfAsync(new()
{
Path = "report.pdf",
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true,
Scale = 1,
Margin = new Margin
{
Top = "16mm",
Right = "14mm",
Bottom = "16mm",
Left = "14mm"
}
});
CSS page sizing
@page {
size: A4;
margin: 16mm 14mm;
}
@media print {
.screen-only { display: none; }
.page-break { break-before: page; }
}
Set PreferCSSPageSize when the document’s @page declaration should take priority over the API’s format, width, or height settings. If the output has unexpected dimensions, inspect both places for conflicting values.
Colors and backgrounds
Print output can modify colors. Use print CSS and the browser’s color-adjust property when exact branded colors matter:
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
Also enable the PDF background option when the document depends on background colors or images. Backgrounds are not guaranteed to appear simply because they are visible in the screen view.
6. Waiting for dynamic content and assets
Export only after the content needed in the PDF is ready. Navigation completion alone may not mean that fonts, images, client-rendered components, or charts have finished.
await page.GotoAsync(url, new()
{
WaitUntil = WaitUntilState.NetworkIdle
});
await page.Locator("main").WaitForAsync();
await page.WaitForTimeoutAsync(500);
await page.PdfAsync(new() { Path = "ready.pdf" });
Prefer waiting for a meaningful selector over using a long fixed delay. Use a short delay only for a known rendering transition, such as a chart animation or web-font swap. For images, wait for the relevant elements or run a readiness check in the page before exporting.
7. Headers, footers, and page ranges
PDF options can include header and footer templates and page ranges. Template scripts are not evaluated, and page styles are not visible inside header or footer templates. Keep templates self-contained and verify their appearance with the actual document.
await page.PdfAsync(new()
{
Path = "pages-2-to-4.pdf",
PageRanges = "2-4",
DisplayHeaderFooter = true,
HeaderTemplate = "<div style='font-size:9px;width:100%;text-align:center'>Report</div>",
FooterTemplate = "<div style='font-size:9px;width:100%;text-align:center'><span class='pageNumber'></span> / <span class='totalPages'></span></div>"
});
8. A reusable conversion method
using Microsoft.Playwright;
public static class HtmlPdf
{
public static async Task<byte[]> ConvertAsync(
string url,
bool useScreenMedia = false,
CancellationToken cancellationToken = default)
{
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(new()
{
Headless = true
});
var page = await browser.NewPageAsync();
await page.GotoAsync(url, new()
{
WaitUntil = WaitUntilState.NetworkIdle,
Timeout = 90_000
});
await page.Locator("body").WaitForAsync();
if (useScreenMedia)
{
await page.EmulateMediaAsync(new() { Media = Media.Screen });
}
return await page.PdfAsync(new()
{
Format = "A4",
PrintBackground = true,
PreferCSSPageSize = true,
Margin = new Margin
{
Top = "16mm",
Right = "14mm",
Bottom = "16mm",
Left = "14mm"
}
});
}
}
For a web service, create browser instances carefully, enforce navigation timeouts, and dispose pages and browsers even when conversion fails. Reusing a browser process while creating isolated contexts can reduce launch overhead, but keep per-request state in a new context.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser launch fails | The matching browser binary is missing. | Run the Playwright browser install command for the installed version. |
| Launch fails in Linux CI | Required system libraries are absent. | Run Playwright’s dependency installation command or install the documented libraries in the image. |
| PDF looks different from the browser | Print media is the default. | Call EmulateMediaAsync with Media.Screen when screen styling is intended. |
| Backgrounds are missing | Background printing is disabled or print CSS removes them. | Enable the background option and inspect @media print rules. |
Output size ignores @page |
API sizing takes precedence. | Set PreferCSSPageSize and remove conflicting format or dimension values. |
| Images or fonts are absent | Resources have not loaded, are blocked, or require authentication. | Wait for the relevant selectors, verify network access, and provide required headers or cookies in the browser context. |
| Content is cut off | Fixed heights, overflow rules, or page-break CSS conflict with print layout. | Inspect print styles, remove restrictive heights, and use break-before/break-inside deliberately. |
| Navigation times out | The site is slow, blocked, or waiting on resources that never finish. | Check the URL from the same runtime, choose a suitable wait condition, and set a bounded timeout. |
10. Performance, reliability, and cost notes
- Startup: launching a browser for every document is simple but adds overhead. A long-lived browser with isolated contexts is usually more efficient for a service.
- Memory: close pages and contexts after each job. Limit concurrency when documents contain large images or complex scripts.
- Reliability: pin the Microsoft.Playwright package version and install its matching browser during deployment. Repeat the browser install step after upgrades.
- Determinism: use stable asset URLs, wait for specific readiness signals, set explicit paper and margin rules, and avoid time-dependent page content where possible.
- Cost: Playwright itself is a software dependency. Your operational costs come from the machine, browser runtime, network requests, and any external services used by the page.
11. Or skip the browser setup
If you need a hosted capture instead of maintaining Playwright binaries and browser infrastructure, ScreenshotNeo provides a website capture API and MCP server. It can return a screenshot or PDF from one GET request. See the ScreenshotNeo API documentation for PDF settings and the full option list.
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}`);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify the page verdict and billing status with headers. Its MCP server includes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots per month at no charge.
12. FAQ
Does Playwright convert HTML to PDF in C#?
Yes. Load a URL or HTML string and call Page.PdfAsync from Microsoft.Playwright.
What media does Playwright use for PDFs?
Print media is used by default. Call EmulateMediaAsync with Media.Screen to use screen CSS.
Can I save the PDF to a file?
Yes. Set the Path option, such as new() { Path = "output.pdf" }.
Why must I install a browser separately?
Playwright controls versioned browser binaries. The binary must match the installed Playwright version.
Can CSS control the paper size?
Yes. Define an @page rule and use the API’s CSS page-size preference when that rule should take priority.


