https://voussoir.net/writing/css_for_printing
Back to writing
CSS for printing to paper
Table of contents
1. Introduction
2. Sample files
3. @page
4. @media print
5. Width, height, margin, and padding
6. Element positioning
7. Multi-page documents with repeating elements
8. Portrait / Landscape mode
9. Data source
10. Essentials cheatsheet
Introduction (SS)
At work, one of the things I do pretty often is write print
generators in HTML to recreate and replace forms that the company has
traditionally done handwritten on paper or in Excel. This allows the
company to move into new web-based tools where the form is autofilled
by URL parameters from our database, while getting the same physical
output everyone's familiar with.
This article explains some of the CSS basics that control how your
webpages look when printed, and a couple of tips and tricks I've
learned that might help you out.
Sample files (SS)
Here are some sample page generators to establish some context, and
perhaps a shred of credibility.
I'll be the first to admit these pages are a little bit ugly and
could use more polish. But they get the job done and I'm still
employed.
Invoice generator
Coversheet with sidebar inputs
Coversheet with contenteditable
QR code generator
@page (SS)
CSS has a rule called @page that informs the browser of your
website's printing preferences. Normally, I use
@page
{
size: Letter portrait;
margin: 0;
}
I will explain why I choose margin: 0 in the later section about
margins. You should use Letter or A4 as appropriate for your
relationship with the metric system.
Setting the size and margin of @page is not the same as setting the
width, height, and margin of your or
element. @page is
beyond the DOM -- it contains the DOM. On the web, your element
is bounded by the edges of your screen, but when printing it is
bounded by @page.
The settings controlled by @page more or less correspond to the
settings you get in your browser's print dialog when you press
Ctrl+P.
Here's a sample file I used to do some experiments:
Sample text
sample text
Here's how that looks in the browser:
[sample_in_]
And here are the results of some different @page values:
@page { size: Letter portrait; margin: 1in; }:
[letter_por]
@page { size: Letter landscape; margin: 1in; }:
[letter_lan]
@page { size: Letter landscape; margin: 0; }:
[letter_lan]
Setting the @page size won't actually put that size of paper into
your printer's feed tray. You'll have to do that part yourself.
Notice how when I set size to A5, my printer stays on Letter, and the
A5 size fits entirely within the Letter size which gives the
appearance of a margin even though it's not coming from the margin
setting.
@page { size: A5 portrait; margin: 0; }:
[a5_portrai]
But if I tell the printer that I have actual A5 paper loaded, then it
looks as expected.
[a5_portrai]
From what I gather by experimentation, Chrome only follows the @page
rule if you have Margin set to Default. As soon as you change Margin
in the print dialog, your output is instead the product of your
physical paper size and the chosen margin.
@page { size: A5 portrait; margin: 0; }:
[a5_portrai]
[a5_portrai]
Even when you choose a @page size that fits fully within your
physical paper, the margin still matters. Here, I make a 5x5 square
with no margin, and a 5x5 square with margin. The size of the
element is bounded by the @page size and margin combined.
@page { size: 5in 5in; margin: 0; }:
[5in_5in_0]
@page { size: 5in 5in; margin: 1in; }:
[5in_5in_1i]
I did all these tests not because I expect to print on A5 or 5x5
paper, but because it took me a while to figure out what exactly
@page is. Now I am pretty confident in always using Letter with
margin 0.
@media print (SS)
There is a media query called print where you can write styles that
only apply during printing. My generator pages often contain a
header, some options, and some help text for the user that obviously
shouldn't come out on the print, so this is where you add
display:none on those elements.
/* Normal styles that appear while you are preparing the document */
header
{
display: block;
}
@media print
{
/* Disappear when you are printing the document */
header
{
display: none;
}
}
[mediaprint]
[mediaprint]
Width, height, margin, and padding (SS)
You'll need to know a bit about the box model to get the margins you
want without wrestling the computer too much.
[box_model]
The reason I always set @page margin: 0 is that I'd rather handle the
margins on the DOM elements instead. When I tried to use @page
margin: 0.5in, I would often accidentally wind up with double-margins
that squash the content smaller than I expected, and my one-page
design spilled onto a second page.
If I wanted to use @page margin, then the actual page content would
need to be laid out all the way up against the edges of the DOM,
which is harder for me to think about and harder to preview before
printing. It is mentally easier for me to remember that
occupies the entire physical paper and my margins are within the DOM
instead of beyond it.
@page
{
size: Letter portrait;
margin: 0;
}
html,
body
{
width: 8.5in;
height: 11in;
}
When it comes to multi-page print generators, you're going to want a
separate DOM element representing each page. Since you can't have
multiple or , you're going to need another element. I
like . Even for single-page generators, you may as well
always use an article.
Since each represents one page, I don't want any margins or
padding on or . We're pushing the logic one step further
-- it is easier for me to let the article occupy the entire physical
page and put my margins within it.
@page
{
size: Letter portrait;
margin: 0;
}
html,
body
{
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
}
When I talk about adding margin within my article, I'm not using the
margin property, I'm using padding. That's because margin goes
outside and around your element in the box model. If you use a margin
of 0.5in, you'll have to set the article to 7.5x10 so that the
article plus 2xmargin equals 8.5x11. And if you want to adjust that
margin you'll have to adjust the other dimensions.
Instead, padding goes on the inside of the element, so I can define
the article to be 8.5x11 with 0.5in padding, and all the elements
inside the article will stay on the page.
A lot of intuition about element dimensions is easier when you set
box-sizing: border-box. It makes it so that the outer dimensions of
the article are locked in while you adjust the inner padding. This is
my snippet:
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
Let's put this all together:
@page
{
size: Letter portrait;
margin: 0;
}
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
html,
body
{
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
}
[margins_pa]
Element positioning (SS)
Once you've got your articles and margins set up, the space inside
the article is yours to do with as you please. Design your document
using whatever HTML/CSS you feel is appropriate for the project.
Sometimes this means laying out elements with flex or grid because
you've been given some leeway with the output. Sometimes it means
creating squares of a specific size to fit on a certain brand of
sticker paper. Sometimes it means absolutely positioning absolutely
everything to the millimeter because the user needs to feed a special
piece of pre-labeled paper through the printer to get your data on
top of it, and you're not in control of that special paper.
I'm not here to give a tutorial on how to write HTML in general, so
you'll need to be able to do that. All I can say is be mindful of
that fact that you're dealing with the limited real estate of a piece
of paper, unlike a browser window which can scroll and zoom to any
length or scale. If your document will contain an arbitrary number of
items, be ready to paginate by creating more .
Multi-page documents with repeating elements (SS)
A lot of the print generators I write contain tabular data, like an
invoice full of line items. If your is large enough to go
onto a second page, the browser will automatically duplicate the
at the top of each page.
| Sample text |
Sample text |
| 0 | 0 |
| 1 | 1 |
| 2 | 4 |
...
[multipage_]
That's great if you're just printing a with no frills, but in
a lot of real scenarios it's not that simple. The document I'm
recreating often has a letterhead on the top of each page, a footer
on the bottom, and other custom elements that need to be explicitly
repeated on each page. If you just print a single long table across
pages, you don't have much ability to place other elements above,
below, and around it on intermediate pages.
So, I generate the pages using javascript, splitting the table into
several smaller ones. The general approach here is this:
1. Treat the elements as disposable and be ready to
regenerate them at any time from objects in memory. All user
input and configuration should take place in a separate header /
options box, outside of the articles.
2. Write a function called new_page that creates a new article
element with the necessary repeating header/footer/etc.
3. Write a function called render_pages that creates the articles
from the base data, calling new_page every time it fills up the
previous one. I usually use offsetTop to see when the content is
getting far along the page, though you could definitely use
smarter techniques to get the perfect fit on each page.
4. Call render_pages whenever the base data changes.
function delete_articles()
{
for (const article of Array.from(document.getElementsByTagName("article")))
{
document.body.removeChild(article);
}
}
function new_page()
{
const article = document.createElement("article");
article.innerHTML = `
`;
document.body.append(article);
return article;
}
function render_pages()
{
delete_articles();
let page = new_page();
let tbody = page.query("table tbody");
for (const line_item of line_items)
{
// I usually pick this threshold by experimentation but you can probably
// do something more rigorously correct.
if (tbody.offsetTop + tbody.offsetParent.offsetTop > 900)
{
page = new_page();
tbody = page.query("table tbody");
}
const tr = document.createElement("tr");
tbody.append(tr);
// ...
}
}
It is usually good to include a "page X of Y" counter on your pages.
Since the number of pages is not known until all pages are generated,
I can't do this during the for loop. I call a function like this at
the end:
function renumber_pages()
{
let pagenumber = 1;
const pages = document.getElementsByTagName("article");
for (const page of pages)
{
page.querySelector(".pagenumber").innerText = pagenumber;
page.querySelector(".totalpages").innerText = pages.length;
pagenumber += 1;
}
}
Portrait / Landscape mode (SS)
I've shown that the @page rule helps inform the browser's default
print settings, but the user can override it if they want to. If you
set @page to portrait mode and the user overrides it to landscape
mode, your layout and pagination might look wrong, especially if you
are hardcoding any page thresholds.
You can accommodate them by creating separate
let print_orientation = "portrait";
function page_orientation_onchange(event)
{
print_orientation = event.target.value.toLocaleLowerCase();
if (print_orientation == "portrait")
{
document.getElementById("style_portrait").setAttribute("media", "all");
document.getElementById("style_landscape").setAttribute("media", "not all");
}
if (print_orientation == "landscape")
{
document.getElementById("style_landscape").setAttribute("media", "all");
document.getElementById("style_portrait").setAttribute("media", "not all");
}
render_printpages();
}
function render_printpages()
{
if (print_orientation == "portrait")
{
// ...
}
else
{
// ...
}
}
Data source (SS)
There are a couple of ways to get your data onto the page. Sometimes,
I pack all of the data into the URL parameters, so the javascript
just does const url_params = new URLSearchParams
(window.location.search); and then a bunch of url_params.get
("title"). This has some advantages:
* The page loads very fast.
* It's easy to debug and experiment by changing the URL.
* The generator works offline.
This also has some disadvantages:
* The URLs become very long and unweildy, people cannot comfortably
email them to each other. See sample links at the top of this
article.
* If the URL does get sent in an email, that data is "locked in",
even if the source record in your database changes later.
* Browsers do have limits on URL length. The limits are pretty high
but not infinite and might vary per client.
Sometimes I instead use javascript to fetch our database records over
the API, so the URL parameters just contain the record's primary key
and maybe a mode setting.
This has some advantages:
* The URLs are much shorter.
* The data is always fresh.
and disadvantages:
* The user has to wait a second while the data is being fetched.
* You have to write more code.
Sometimes I set contenteditable on the articles so the user can make
small changes before printing. I also like to use real, live checkbox
inputs they can click before printing. These features add some
convenience, but in most cases it would be wiser to make the user
change the source record in the database first. Also, they limit your
ability to treat the article elements as disposable.
Essentials cheatsheet (SS)
sample_cheatsheet.html
Sample page 1
sample text
Sample page 2
sample text
---------------------------------------------------------------------
View this document's history
* 2024-03-03 Add css_for_printing.md.
Contact me: writing@voussoir.net
If you would like to subscribe for more, add this to your RSS reader:
https://voussoir.net/writing/writing.atom