Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Chrome rewrites our text colours when it prints, and the heading is the one it gets wrong

PubTrivia is a quiz night app, and quiz nights involve paper. People print the running order, the scoresheets, the answer sheets. So it is slightly embarrassing that our stylesheet contains this many rules about printing

PubTrivia is a quiz night app, and quiz nights involve paper. People print the running order, the scoresheets, the answer sheets. So it is slightly embarrassing that our stylesheet contains this many rules about printing:

$ for k in '@media print' '@page' 'break-inside' 'page-break' 'orphans' 'widows' 'print-color-adjust'; do
    printf "%-20s %s\n" "$k" "$(grep -o "$k" css_*.css | wc -l)"
  done
@media print         0
@page                0
break-inside         0
page-break           0
orphans              0
widows               0
print-color-adjust   0

Zero, in 107,598 bytes across our two stylesheets. I spent an evening finding out what that actually produces, using Page.printToPDF over CDP rather than guessing.

Nothing changes under print media

First the control. Emulating print media and re-reading computed styles on eight pages:

await page.cdp("Emulation.setEmulatedMedia", { media: "print" });

matchMedia('print').matches goes true, and then nothing moves. Same body colour, same paragraph colour, same scrollHeight to the pixel, on every page. Which is what zero print rules means, but it is worth confirming before blaming the browser for anything that follows.

What a page costs in paper

A4, default margins, backgrounds off, which is what the Chrome print dialog gives you unless you go looking:

Turning backgrounds on changes almost nothing in file size except on the pricing page, which goes from 327,716 to 400,184 bytes because it is the one page carrying real gradients.

The colour remap, measured

Our public pages are dark: background rgb(23, 23, 23), headings rgb(250, 250, 250), body paragraphs rgb(153, 153, 153). I assumed printing without backgrounds would give white paper with near-white headings on it, which would be a disaster, and I wrote half this post on that assumption before actually looking at the output.

So I rendered the PDFs to PNG and sampled the glyph pixels, compositing the alpha over white, which is the only way to be sure what colour actually landed.

With printBackground: true:

band sampled
page background (23, 23, 23)
heading glyphs (250, 250, 250)
body glyphs (153, 153, 153)

Exactly the authored values, which is a useful check that my sampler is not lying.

With printBackground: false, the default:

band sampled contrast on white
page background (255, 255, 255)
heading glyphs (166, 166, 166) 2.43:1
body glyphs (69, 69, 69) 9.59:1

Chrome does not simply drop the background and keep the text. It darkens light text so that something legible survives, and it does it unevenly. Our body text goes from 6.29:1 against our dark background to 9.59:1 against paper, which is better than it was. Our headings go from 17.18:1 to 2.43:1, which fails WCAG AA even at large sizes, and they are now the palest thing on the page. The brightest element on screen is the hardest to read on paper, and the ordering between heading and body has been inverted by a renderer heuristic nobody in this repository has an opinion about.

The control for that claim is inside the same product. Our three legal pages are the only ones wrapped in a light container:

$ grep -l '"light ' html/*.html
html/_legal_cookies.html
html/_legal_privacy.html
html/_legal_terms.html

They are dark text on white already, and their printed glyphs sample at (3, 8, 22) against an authored rgb(2, 8, 23). Untouched. So the remap is specifically Chrome rescuing light-on-dark text when the dark has been thrown away, and the three pages nobody wants to print are the three that print correctly.

The header prints on every sheet, over the words

Every public page has two position: fixed elements: the wrapper around the navigation bar, and the toast region. A fixed element in a Chrome print job is painted at the top of every sheet. That means page two of our round planner looks like this: the navigation bar, and under it, half-covered, the row of the form that happened to land at that break.

It is not an overlap you can argue about. The top line of content on sheets two through five is clipped behind a nav bar that a sheet of paper cannot scroll.

Check it yourself in about ten seconds, no account needed: open the round planner, press Cmd+P or Ctrl+P, and page down to sheet two.

The thing people would actually print is on sheet two

Sheet two is also where the output lives. The planner builds a running order: a start time, each round with its length, where the break goes, and the finish time. It is the single most printable artefact we have published, a quizmaster's running sheet for the evening, and to get it on paper today you print five sheets, discard four, and accept a nav bar across the top of the one you keep.

Meanwhile our scoresheet generator avoids the whole subject by building a PDF in JavaScript with jsPDF and handing you a download. That was the right call for a table grid with exact dimensions. It also means the one part of this product that treats paper seriously never touches the browser's print path at all, which is probably why nobody noticed the print path was unattended.

The fix is about twenty lines

Nothing here needs a redesign:

@media print {
  /* use the light palette the legal pages already prove works */
  :root { color-scheme: light; }
  /* nothing fixed survives paper */
  header, [role="region"][aria-label^="Notifications"] { display: none !important; }
  /* keep a round and its description together, whatever wraps each row */
  li { break-inside: avoid; }
  @page { margin: 14mm; }
}

Plus a print button on the planner that prints only the running order, which is the bit of this that is worth doing properly.

Two lessons I would take to any site. First, @media print emulation plus printToPDF is a five-minute audit and nobody runs it, so whatever your site does on paper, it does by accident. Second, render the PDF and sample the pixels rather than reasoning from your CSS values, because the browser is allowed to change them on the way out, and in our case it changed both of ours and improved one of them.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.