Ken Harris needed PDF export for assessment reports at EdisonOS. Every model he asked proposed one of two solutions: a server-side job queue with a headless browser rendering HTML to PDF, or a client-side library like html2pdf.js that snapshots the DOM and stitches images into a PDF.
He shipped it with under 300 lines of Print CSS instead.
The two standard answers are not stupid. The server-side route gives control and flexibility, at the cost of maintaining HTML/CSS templates that drift from the frontend, expensive per-export rendering, headless-browser deployments to patch, and a larger attack surface. The client-side route avoids the server, but produces image-based PDFs: no text search, no copy, larger files, awkward multi-page layout, and it can lag the user's machine.
What neither does is ask what the browser already does.
What the browser already does
Every browser can print. Every browser can print to PDF. What it needs is a document laid out for paper rather than for a screen with tabs, dropdowns and interactive charts.
So when the user clicks Export, they render a printable version of the report — same components, same charts — through a React portal into document.body, hidden off-screen at exactly A4 width so the charts render at print size. A class on <body> hides the rest of the app during printing; print CSS only applies when that class is present.
Then they wait. This is the part that separates a demo from a feature:
- Two
requestAnimationFramecalls, because React mounting the portal doesn't mean the browser has painted it. document.fonts.ready, because otherwise the PDF uses fallback fonts.image.decode()on every image in the document, because the logo is a plain<img>with a presigned URL and can simply not be there when the print dialog opens.
They also swap document.title while printing, because that's the filename the browser suggests — you don't want to hand a tutor Dashboard.pdf. And they restore it in a finally block, along with the body class.
None of that is exotic. All of it is the kind of detail you learn by shipping a print path once.
What Print CSS can do
More than most people expect. Set paper size and margins. Start each module on a new page. Avoid splitting cards and charts across pages. Prevent headings from being stranded at the bottom. Repeat table headers on every page for long tables.
That last one is a genuinely better result than the image-snapshot approach, and it costs a few lines of CSS rather than a pagination engine.
Why the models missed it
This is the interesting part, and it isn't really about models.
PDF export is a solved problem in the training data, and the solutions that dominate the corpus are libraries and server-side rendering. Print CSS is old, unglamorous, and mostly documented in specifications and MDN pages rather than in enthusiastic blog posts. A model optimizing for the most likely correct answer will propose the most common one, and the most common one is the heavy one.
Harris's own conclusion is sharper: web developers complicate things, and LLMs have picked up the habit. Most problems are simple, so are good solutions. The missing move is asking "what does the platform already do?" before asking "which library?"
That's a real skill and it's under-rewarded, because platform features don't have marketing.
The limits
He's honest about them. The user has to go through the print dialog, which makes bulk export awkward. You inherit each browser's print engine, and WebKit is its own adventure. It worked for a tutor downloading one report and emailing it — which was the actual use case.
That's the right way to frame a trade: not "this is better," but "this is better for this shape of problem." A batch export service would still want a server-side renderer.
What I take from it
Three things.
Print is a second rendering target. The timing rules are different — fonts and images and paint order — and the details matter more than the CSS.
The simplest solution is often invisible because it has no ecosystem around it. Searching for a library finds libraries; reading a spec finds the platform.
"Under 300 lines" is the metric that matters. Not because line count is virtue, but because the maintenance cost of the two alternatives — template drift, headless browsers, image-based PDFs — is ongoing, and the print solution's cost is roughly zero.
The browser already knows how to print. The work is making a document worth printing.