Print stylesheets

If you’re reading this on a device with a keyboard, press Ctrl+P now. On most browsers and systems, that should bring up a Print Preview dialogue for printing this webpage. You should notice quite a few differences between how this blog post appears in your browser and how it appears on the page. For one thing, the text is split into two columns, and for another, many parts of the page – the site header, footer and several buttons – have disappeared. What is this wizardry, you may ask? As the title of this post indicates, it’s all about print stylesheets.

Media queries and the promise of @page

Media queries have been an important tool for building responsive CSS since their introduction with CSS 3 in 2012. Many of their original uses have since been superseded by natively responsive constructs such as Flexbox, but they’re still the best way to make specific and potentially quite radical changes in layout between different screen sizes. For example, I use the rule below to display description lists in two columns on larger displays, as opposed to their browser-default vertical appearance:

@media (min-width: 600px) {
  dd { grid-column: 2; }

I’ve also used media queries (and CSS variables) to create a dark mode for this site without the need for a day/night button and all the extra JS that would go with that:

@media (prefers-color-scheme: dark) {
  /* ... */

Both of the above snippets use media queries without a media type, but perhaps you’ve seen directives that look like this before:

@media screen and (min-width: 600px) {
  /* ... */

As it turns out, media queries are a modern extension to media types, which was first introduced in CSS 2.1. Before media queries, all you could do was write rules like this:

@media screen {
  body {
    color: black;
    background-color: white;

Or this:

@media print {
  h1 {
    page-break-before: always;

Wait a minute, page break? In CSS? Yes – the purpose of media types is to apply different styles for different media, including screens, printed pages and more1. Here’s an Eric Meyer blog post from January 2000 on writing stylesheet rules for print so that you don’t have to maintain a separate printer-friendly version of each page on your site (remember those?).

Print stylesheets have been with us since the early days of the web, but they’ve never been a very well-known or publicised feature of CSS. Apart from @media print and paged media properties dealing with page breaks, the other key element for creating a print stylesheet is @page, an at-rule for controlling aspects of individual printed pages.

When I was putting together this site’s footnote previews, I came across this Smashing Magazine article, which made it seem as though I might be able to implement per-page footnotes for printed versions of my site. However, a bit of experimenting followed by a more careful read of the article revealed that the majority of @page’s proposed features have not been implemented in any mainstream browser. As of now, you can configure how big your page is, what its margins should be, and whether it ought to be portrait or landscape, and you can also do different things with left and right pages. The footnote rules and the margin rules intended for creating page headers and footers do nothing.

So, after excitedly implementing a bunch of print stylesheet rules, I abandoned my attempt at full printer-friendliness and forgot about the subject for the next three years.

Enter Paged.js

Recently, a project unrelated to this website gave me cause to think about this print stylesheet stuff again. While browsers don’t support most of this functionality, it’s a defined specification that has been implemented by several libraries and paid services for turning HTML into PDFs and books. The website provides a wealth of information on the subject as well as a handy index of features showing which implementation supports what. There’s also a companion playground site that lets you test out the different implementations.

As far as free-to-use implementations go, Paged.js appears to be the most fully featured and well documented. It can be used as a polyfill script or through the command line. The polyfill script converts the page where it’s injected into an on-screen simulation of a printed document – you end up with your entire webpage in a tiny corner with scrollbars and page separators. You can then save the page as a PDF (or print it directly) using Ctrl+P, and this time the preview will show all those nice @page features.

My preferred method, though, is to use the CLI, which converts any HTML file to a PDF in one step:

pagedjs-cli input.html -o output.pdf

Under the hood, this injects the Paged.js polyfill script into input.html’s <head>, serves it to a headless Chromium and prints to PDF. Note that input.html should not already be using the polyfill – this causes crazy things to happen.

For the project I was working on, my pre-Paged.js print stylesheet had:

  • A couple of page-break-before rules for specific heading elements
  • @page size and margins
  • various @media print rules that followed the opposite of responsive design principles – font-size specified in pt rather than em and sizes of elements in fixed mm and cm. It’s rather freeing to write CSS for a page that you know won’t ever change in width or height.

From there, I delved into Paged.js-specific features such as page headers and footers, and a table of contents complete with page numbers.

I also had to deal with a few bugs (mostly of my own making). Here’s what I learnt:

  1. Writing CSS for a JavaScript polyfill provided some harsh reminders about how forgiving browsers are in comparison. Leave the second colon off a pseudo-element (e.g. h1:before instead of h1::before) and your browser will understand what you mean and apply the rule, but Paged.js won’t. Similarly, the browser will let you increment a counter in a pseudo-element, whereas Paged.js will not.
  2. MDN will tell you that page-break-after|before|inside is deprecated and you should use break-after|before|inside instead, but Paged.js still very much expects that preceding page-.
  3. There are many small differences between how a given browser will render a page for printing and how Paged.js will do it, beyond the additional feature support. For example, Chrome is a lot better at printing tables – it will automatically repeat table headers at the top of new pages for long-running tables,2 and is less prone to cutting off rows.3
  4. There are some non-obvious things Paged.js doesn’t support, such as CSS Grid, and some things it supports that Chrome doesn’t, such as element().4
  5. The most maddening thing about working with Paged.js is dealing with multi-page tables.5 At the time I’m writing this, the overflow detection is quite buggy, and a table with too much padding will just lose rows off the end of a page. It will still continue on the next page, though, so you’ll end up with, say, rows 1-8 on the first page and rows 11-14 on the second, with no sign of the missing two in the middle. In the end, I gave up on vertical padding in table cells beyond a pixel or two.6
  6. The second most maddening thing about working with Paged.js is dealing with multi-page codeblocks (<pre>). I had to set display: inline and then do all kinds of weird things to get the background colour back before they would split normally across pages like they do in Chrome’s print preview.
  7. The differences between Paged.js and browser printing mean that, at some point, you’re likely to end up with three sets of stylesheet rules: what to show on screen, what to show in browser print and what to show in Paged.js. And by the very nature of what Paged.js does – simulating @media print on @media screen – there’s no way to differentiate between those last two, so we can say goodbye to the dream of a single stylesheet.

Final thoughts

My goal with making a print stylesheet for this site was to have some neat surprises for anyone who attempted to print a page from it. As of today, most of the neat surprises I would have liked to include would only be displayed through a third-party library, which would spoil them. Paged.js and its competitors are very much dev-oriented internal tools that wouldn’t make sense to implement here.

That said, this has been a very useful discovery for other projects, and I’m heartened to see such actively developed and feature-rich libraries for producing print media with HTML. I’ve done my time in the LaTeX mines and it is with a wealth of experience that I say I’d rather use HTML and CSS for my document finagling. So I’m glad there’s a decent way to do that for print (and fake print).7

Useful resources

  1. All media types except print, screen and all have since been deprecated↩︎

  2. To do this in Paged.js, you need to use some JavaScript code from an issue on their Gitlab. To run the code with pagedjs-cli, save it to a file and use the flag --additional-script repeatingTableHeaders.js↩︎

  3. Both Chrome and Paged.js will split one table cell across multiple pages sometimes though, even with break-inside: avoid↩︎

  4. Firefox remains the only major browser to support this crazy CSS feature (which is very useful in Paged.js for creating page headers and footers). ↩︎

  5. You could probably say the same thing about LaTeX and many other document formatting systems. Tables, man. ↩︎

  6. There’s a massive PR of pagination fixes open right now, so hopefully this gets resolved for the next release. ↩︎

  7. “Very well then I contradict myself…” ↩︎

similar posts