Typesetting a Resume with HTML and CSS

With my PhD drawing to a close, I'm on the job market! I'll need a slick resume ASAP, but how to typeset it?

Microsoft Word?
...don't have it.
...don't have the patience for it.
...now that could work!

I figured I would merely whip up a document of the appropriate size:

<!DOCTYPE html>
<html lang="en">
      html, body {
        margin: 0;
        padding: 0;

      .page {
        width: 8.5in;
        height: 11in;
        padding: 0.5in;
        outline: 1px black solid;
    <div class="page">
      Hire me, please!

...maybe grab some Google Fonts (way easier than managing system fonts, or fiddling with LaTeX!), open up DevTools, and bust out a sweet-looking resume. Then, I'd simply use the browser dialog to print to PDF (taking care to set the print margins to 0).

Oh, if it were only that simple.


CSS Units ≠ Physical Units

After hours of writing and styling, I produced what I thought was the perfect resume, tuned to fit into a single page. Then, I went to print-to-PDF. Disaster unfolded before my eyes: the fonts and spacing were all wrong, and my carefully-constructed columns of prose spilled awkwardly onto a second page. Why?

A CSS inch (as will all "physical" CSS units) has no relation to a physical inch. Rather, it's defined to be exactly 96 CSS pixels. (And, yes, that's 96 CSS pixels, which aren't necessarily equal to device pixels.)

At first, I feared that I would need to figure out the mapping between CSS pixels, dots, and inches. Fortunately, most web browsers do map CSS inches to physical inches when printing. So, all I needed to do was design for what appeared in Chrome's print preview, not what appeared in the browser tab.

No DevTools, Quick Reload with Print Preview

It immediately become apparent that DevTools cannot be used while the print dialog is open. Instead, I needed to CTRL+R-and-CTRL+P after every edit-and-save of my resume. This got old fast.

I decided to grin and bear it. How much longer could this possibly take?

Font Inconsistencies

"Huh, I could have sworn I had made that text bold."

I had.

Chrome's print-to-PDF respected the font-weights I had set for my Libre Baskerville headings, but not the weights I had set on my Raleway prose. The issue wasn't limited to bold spans; all of the body text was far too light!

A quick Google search revealed no leads, and the looming specter of LaTeX drew nearer...


I recalled that there's another way to get PDFs out of Google Chrome: Puppeteer, a library for programatically controlling the browser. I had recently used this API to generate vector screenshots of a browser-based IDE for a research paper, and I hadn't noticed any font issues then.

It worked. No font issues on my resume!

So, I decided to take another fifteen minutes to fix my other gripe: no live preview. I spent another fifteen minutes building a reusable utility that would:

  • Consume two command-line arguments: the path to an HTML input file, and a path to save the outputted PDF to.
  • Watch the input file for changes.
  • Re-render the page as a PDF to the output path upon each change.

The script is invoked like this:

$ node resume-screenshot.js ~/documents/resume.html /tmp/resume.pdf

I then opened zathura, a PDF viewer that reloads the viewed file each time it's changed, and got back to hacking on my resume! I arranged my HTML editor and zathura side-by-side, and each edit-and-save I make to my resume is nearly-instantly reflected in the zathura.

Here's the utility, in all of its glory:

const chokidar = require('chokidar'); // ^3.5.1
const puppeteer = require('puppeteer-core'); // ^3.1.0

let [input, output] = process.argv.slice(2);

(async () => { try {
  let browser = await puppeteer.launch({
    headless: true,
    executablePath: '/opt/google/chrome/chrome', /* CHANGE ME */

  const page = await browser.newPage();

  async function render_pdf(input) {
    await page.goto(`file://${input}`, { waitUntil: 'networkidle0' });
    await page.pdf({
      printBackground: true,
      width: '8.5in',
      height: '11in',
      path: output,
    console.log("saved:", output);

    .on('ready', () => console.log("watching", input))
    .on('add', render_pdf).on('change', render_pdf)
    .on('unlink', (path) => process.exit(1))

} catch(e) {

Other Tips


I have no doubt that a LaTeX or InDesign expert could produce a nicer looking document. The line-breaking employed by web browsers is poorer than that of LaTeX, and advanced microtypography is not readily available. Nonetheless, I am extremely pleased with the overall design of my resume. The ease of using HTML and CSS allowed me to attempt a much more ambitious design than I would have attempted in LaTeX.

I've uploaded the HTML of my (in-progress) resume here, and the resulting PDF here:

Email comments and corrections to jack@wrenn.fyi.