CSS for printing on paper

Introduction

In my job, I quite often create HTML print generators to recreate and replace forms that a company traditionally filled out by hand on paper or in Excel. This allows the company to move to new web tools where the form is automatically populated with URL parameters from our database, while creating the same paper output that everyone is used to.

In this article, I'll explain the basic CSS that controls the appearance of web pages when printed, and give you a couple of tips that can help you with that.

Example files

Here are some examples of page generators to give you some context.

To begin with, I will admit that these pages are a little ugly and could use improvement. But they get the job done and I still haven’t been fired.

Invoice generator

Covering note with input in side column

Covering note with contenteditable

QR code generator

@page

There is a rule in CSS @page, which tells the browser the website's printing preferences. I usually use

@page
{
    size: Letter portrait;
    margin: 0;
}

Why did I choose margin: 0, will be explained below. You should use Letter or A4, depending on 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 an element <html> or <body>. @page is outside the DOM – it contains the DOM. On the web element <html> limited to the edges of the screen, and when printing it is limited to @page.

The parameters controlled by @page more or less correspond to the parameters in the browser's print dialog box, which opens when you press Ctrl+P.

Here is an example file that I used for experiments:

<!DOCTYPE html>
<html>
<style>
@page
{
    /* см. ниже информацию по каждому из экспериментов */
}
html
{
    width: 100%;
    height: 100%;
    background-color: lightblue;

    /* сетка создана shunryu111 https://stackoverflow.com/a/32861765/5430534 */
    background-size: 0.25in 0.25in;
    background-image:
    linear-gradient(to right, gray 1px, transparent 1px),
    linear-gradient(to bottom, gray 1px, transparent 1px);
}
</style>
<body>
    <h1>Sample text</h1>
    <p>sample text</p>
</body>
</html>

Here's what it looks like in the browser:

but i don't want new chrome

And here are the results for different @page values:

@page { size: Letter portrait; margin: 1in; }:

@page { size: Letter landscape; margin: 1in; }:

@page { size: Letter landscape; margin: 0; }:

Setting the @page size will not actually set that size in the printer tray. You will have to change it yourself.

Please note that when I set as size A5, my printer is left at Letter, and the A5 fits entirely within Letter, giving the appearance of having margins even though they are not taken from the setting margin.

@page { size: A5 portrait; margin: 0; }:

But if I tell the printer that A5 paper is loaded, then everything looks as expected.

From experiments, it turned out that Chrome follows the @page rule only if Margin is set to Default. Once you change the Margin in the print dialog, the result will be derived from the physical paper size and the selected margins.

@page { size: A5 portrait; margin: 0; }:

Even if you choose a @page size that fits entirely within the physical size of the paper, margin still important. Here is an example of a 5×5 square without borders and a 5×5 square with borders. Item size <html> limited by total @page size And fields.

@page { size: 5in 5in; margin: 0; }:

@page { size: 5in 5in; margin: 1in; }:

That's not why I did all these tests. that I want to print on A5 or 5×5 paper, but because it took me quite a long time to figure out what @page. Now I'm sure I should always use Letter with margin 0.

Print @media

Exists media query print, which allows you to record styles that are applied only during printing. My generator pages often contain a title, various options, and help text for the user; obviously they should not be printed, so we add here display:none for these elements.

/* Обычные стили, отображаемые при подготовке документа */
header
{
    display: block;
}

@media print
{
    /* Пропадают при печати документа */
    header
    {
        display: none;
    }
}

Width, height, margins and padding

To avoid struggling with the computer too long to get the fields you need, you need to learn about framework model.

I always set @page margin: 0, because I'd rather handle the fields in the DOM elements themselves. When I tried to set @page margin: 0.5insometimes I ended up with double margins that compressed the content and made it smaller than expected, causing my one-page design to partially carry over to the second page.

If I wanted to use @page fields, the page content would need to be lined up to the DOM boundaries; it's harder for me to think about and it's harder to display in the print preview. It's easier for me to remember that <html> takes up all the physical paper, and my fields are inside the DOM, not outside it.

@page
{
    size: Letter portrait;
    margin: 0;
}
html,
body
{
    width: 8.5in;
    height: 11in;
}

When working with multi-page print generators, it is worth creating a separate DOM element for each page. Since several <html> And <body> It can't be, we'll need another element. I like <article>. It can be used anytime, even for single page generators.

Since everyone <article> means one page, I don't need margins and padding in <html> And <body>. We take the logic one step further – it's easier to make the article take up the entire physical page and put the fields inside it.

@page
{
    size: Letter portrait;
    margin: 0;
}
html,
body
{
    margin: 0;
}

article
{
    width: 8.5in;
    height: 11in;
}

When adding fields to an article, I do not use the property marginA padding. The reason is that margin extends outside the element in the box model. If you use margin to 0.5in, then you need to set article to 7.5×10 so that article plus 2xmargin equals 8.5×11. And if you want to change these fields, you will have to change other dimensions as well.

padding is inside an element, so you can set article to 8.5×11 with 0.5in padding and all elements inside article will remain on the page.

Understanding the dimensions of elements becomes much easier if you set box-sizing: border-box. In this case, the external dimensions of the article are fixed when setting the internal indentation. Here is my code snippet:

html
{
    box-sizing: border-box;
}
*, *:before, *:after
{
    box-sizing: inherit;
}

Now let's put it 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;
    padding: 0.5in;
}

Positioning elements

After setting up the article and margin, you can customize the space inside the article as you please. Create a document design using any HTML/CSS that seems appropriate for your project. Sometimes this means placing elements using flex or grid because you've been given a certain amount of leeway with the result. Sometimes this means creating squares of a specific size to fit on a specific brand of sticker paper. Sometimes this requires positioning everything in absolute terms, down to the millimeter, because the user needs to run a special piece of paper through the printer to print your data on top of it, and you have no control over that piece of paper.

I won't give advice on writing HTML here, so you should be able to do it yourself. I can only say that you need to remember that you are dealing with limited space on paper, and not with a browser window that can be scrolled and zoomed to any length or scale. If the document contains an arbitrary number of elements, be prepared to implement pagination by creating additional <article>.

Multi-page documents with repeating elements

Many of the print generators I create contain tabular data, such as an invoice with line items. If your <table> large and goes to the next page, then the browser automatically duplicates <thead> at the beginning of each page.

<table>
    <thead>
        <tr>
            <th>Sample text</th>
            <th>Sample text</th>
        </tr>
    </thead>
    <tbody>
        <tr><td>0</td><td>0</td></tr>
        <tr><td>1</td><td>1</td></tr>
        <tr><td>2</td><td>4</td></tr>
        ...
    </tbody>
</table>

This is great if you're just typing <table> without unnecessary embellishment, but in many real situations everything is not so simple. The document I reproduce often has a printed heading at the top of each page, a footnote at the bottom, and other special elements that must be repeated on each page. If you simply print one long table across all pages, there is virtually no way you can fit other elements above, below, and around the pages in between.

So I generate pages using Javascript, breaking the table into several smaller ones. In general I use this approach:

  1. Handling the elements <article> as with disposable ones and I’m preparing to regenerate them at any time from objects in memory. All user input and settings should occur in a separate header/options block, outside of all articles.

  2. I'm writing a function new_page creating a new article element with all the necessary repeating headings/footnotes and so on.

  3. I'm writing a function render_pageswhich creates an article from the base data, I call new_page every time. when she completes the previous article. I usually use offsetTop to control when content goes too far across the page, but you can definitely use smarter techniques to ensure perfect placement on every page.

  4. I call render_pages whenever the underlying 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 = `
    <header>...</header>
    <table>...</table>
    <footer>...</footer>
    `;
    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)
    {
        // Обычно я подбираю это пороговое значение экспериментально, но, вероятно, можно
        // задать что-то более строгое.
        if (tbody.offsetTop + tbody.offsetParent.offsetTop > 900)
        {
            page = new_page();
            tbody = page.query("table tbody");
        }
        const tr = document.createElement("tr");
        tbody.append(tr);
        // ...
    }
}

It's usually a good idea to add a “page X of Y” counter to your pages. Since the number of pages is unknown until all pages have been generated, this cannot be done in a for loop. At the end I call the following function:

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

I showed that the @page rule helps configure the browser's default print settings, but the user can override them if desired. If you set @page to portrait mode and the user selects landscape mode, the layout and pagination may not look right, especially if you are hard-pressing all page thresholds.

You can take this into account by creating separate elements <style> for portrait and landscape modes and switching between them using Javascript. There may be a nicer way to do this, but @rules like @page behave differently than regular CSS properties, so I'm not sure. You also need to store some kind of variable that will allow the function render_pages work correctly.

You can also stop setting threshold values ​​rigidly, but then I would have to follow my own recommendation.

<select onchange="return page_orientation_onchange(event);">
    <option selected>Portrait</option>
    <option>Landscape</option>
</select>
<style id="style_portrait" media="all">
@page
{
    size: Letter portrait;
    margin: 0;
}
article
{
    width: 8.5in;
    height: 11in;
}
</style>

<style id="style_landscape" media="not all">
@page
{
    size: Letter landscape;
    margin: 0;
}
article
{
    width: 11in;
    height: 8.5in;
}
</style>
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

There are a couple of ways to transfer data to pages. Sometimes I wrap all the data in URL parameters so that Javascript just does const url_params = new URLSearchParams(window.location.search);and then several operations of the form url_params.get("title"). This approach has the following advantages:

  • The page loads very quickly.

  • It's easy to debug and experiment by changing the URL.

  • The generator works offline.

But it also has disadvantages:

  • URLs become very long and chaotic, making it difficult for people to email them conveniently. See examples of links at the beginning of this article.

  • If the URL is sent via email, then the data is “fixed” even if the original database entry is later changed.

  • Browsers have a limit on URL length. It is quite large, but not infinite and may vary among clients.

Sometimes I use Javascript instead to get database records via an API, so the URL parameters only contain a primary key and sometimes a mode parameter.

This has its advantages:

and cons:

  • The user has to wait before the data is received.

  • You need to write code.

Sometimes I set for article contenteditableto allow the user to make small changes before printing. Plus, I like to use actual, actionable checkboxes that you can click on before printing. These features improve convenience, but in most cases it is better to have the user change the original record in the database first. Additionally, this limits the ability to treat article elements as disposable ones.

Cheat sheet for the most important things

sample_cheatsheet.html

<!DOCTYPE html>
<html>
<style>
@page
{
    size: Letter portrait;
    margin: 0;
}
html
{
    box-sizing: border-box;
}
*, *:before, *:after
{
    box-sizing: inherit;
}

html,
body
{
    margin: 0;
    background-color: lightblue;
}

header
{
    background-color: white;
    max-width: 8.5in;
    margin: 8px auto;
    padding: 8px;
}

article
{
    background-color: white;
    padding: 0.5in;
    width: 8.5in;
    height: 11in;

    /* Для центрирования страницы на экране в процессе подготовки */
    margin: 8px auto;
}

@media print
{
    html,
    body
    {
        background-color: white !important;
    }
    body > header
    {
        display: none;
    }
    article
    {
        margin: 0 !important;
    }
}
</style>

<body>
    <header>
        <p>Текст подсказки с объяснением задачи этого генератора.</p>
        <p><button onclick="return window.print();">Print</button></p>
    </header>

    <article>
        <h1>Sample page 1</h1>
        <p>sample text</p>
    </article>

    <article>
        <h1>Sample page 2</h1>
        <p>sample text</p>
    </article>
</body>
</html>

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *