How to Print a Repeating Header from the Browser

This post should also be called, “Damnit, Internet Explorer: you sneaky bastard.”

Scenario

We have an oldish web data entry system for a client we recently updated.  One of the features of this site is the ability to print a “Report,” curating previously-entered data.  These Reports have a header with specific information about each report, and it was necessary for these headers to repeat on each page of said report.

Specifications

  • Print the same header at the top of every Report page
  • Print it from the browser
  • HTML page to print includes several Reports on one page.  Therefore, we need something that
    • Repeats the Report’s header only on the pages for that Report
    • Reports shouldn’t appear on the same printed pages; there needs to be a page break between each Report.

Basically, something like this:

Enter the problem

No PHP or Open Source

This system was created in Visual Studio (i.e. the bane of my existence) a million years ago on ASP.NET/C#, and as our client has the slimmest shoestring budget known in the public sector, it had to remain.  No PHP. No open source platforms.

No Money

Further, we had zero dollars to spend on PDF-generating book publisher software and no PHP to deploy open-source options.  Plus, we were basically doing these updates for free.  On a rush.

@page: you keep me wishing

After scouring the Internet for days, I thought I was going to find a solution in a combination of @page, margin-boxes and css-generated content().

I found these tutorials from Smashing Mag very promising:

It seemed simple enough: use @page and the margin-box model to specify content, divs, page numbers, etc. for @media print.  Hooray!

Maybe one day?

Tested in Chrome: nothing. Tested in Firefox: zilch. Tested in IE: nada!

I can’t quite figure out if @page is old technology or things yet to come.  Some resources I found made it seem like it was CSS for very specific, non-browser uses or something that was supported, but not anymore.

I have no idea.  I tested, tested, tested.  Some things would work in Firefox, but not everything.  Somethings would work in Chrome, but only on Mac.  But nothing that could be trusted for client deployment.

Anyone have any thoughts about this? Have you found a successful, cross-browser way to use @page for this type of problem?

Please let me know in the comments if you have!

Blast from the Past: Using Tables for Structure

To my and everyone on my team’s surprise, the most useful method of getting repeating headers when printing out reports from the browser was a table.

That’s right, an old-school, 1994 table layout like I was building my first geocities site. Building a huge table to contain everything in super tall <td>s.  TABLES!

Well…that’s not total, 100% true, because I utilized @media print and some regular old <div> containers, but still! TABLES!

And to further my disbelief, this only works in Internet Explorer or Firefox on Windows! INTERNET EXPLORER! IE, you sneaky, stupid jerk!

Jimmy-rigging the thead

Turns out, IE and Firefox will repeat the thead on every page a table appears when the table spreads over more than one page. Excel also has this options, as do most spreadsheet software, I think, going back to the beginning of digital spreadsheets.

In the traditional sense, this means if you have a giant HTML table with accounting information, for example, you column headers will appear at the top of every printed page.  Handy!

After reading some more Stacks about table layouts, I figured out if I put the information I needed to repeat within a thead of a larger table, I could trick the browser into printing it on each page.

Show us the code already!

Building the layout table

Each Report has its own table, which is used as an identifier in the CSS and as the layout in the HTML.  Here, it’s the class .report-container.  

This table contains three sections, two of which can repeat on each printed page:

  1. <thead> Repeats
  2. <tfoot> Repeats
  3. <tbody> Does not repeat

Here’s the basic set-up

<table class="report-container">
  <thead class="report-header"> </thead>
  <tfoot class="report-footer"> </tfoot>
  <tbody class="report-content"> </tbody>
</table>

Here’s a visualization of the layout:

Each Report is wrapped in the <table class="report-container">, with the repeating information inside <thead>.

Creating the Header

Everything we want to repeat at the top of each printed page will go inside a row and cell of <thead>.

In our case, I put everything inside a <div> inside the cell inside the row inside of <thead>, just so I could control more things for our client and their specifications.

<thead class="report-header">
   <tr>
      <th>
         <div class="header-info">
         ...
         </div>
      </th>
   </tr>
</thead>
Things to note:
  • There’s only one cell/column, <th>, and one row, <tr>, in my example, but really could have as many as you need.
  • Every <th> must live inside a <tr>, though, to work
  • <thead> must appear first in your <table>
  • You can really have whatever you want appear in the <div class="header-info"> within the <th>, including other tables.  It’s like it’s 1996 again!

Adding a Footer

You can also utilize the <tfoot> tag to display content at the bottom of every page.  In the combination of <tfoot>and CSS, you can style it however you want, including applying margins, :after / :before pseudo-classes and positioning.

<tfoot class="report-footer">  
   <tr> 
      <td>
        <div class="footer-info">
        ...
        </div>
      </td>
   </tr>
</tfoot>

! Very Important! <tfoot> must be after <thead> but before <tbody>, even though it renders after <tbody>.

Creating the Main Content Area

All of the information you don’t need to repeat simply goes in the tbody tag within the table.  I contain all of this in a div.  Again, this can be whatever you need it to be!

<tbody class="report-content"> 
   <tr> 
      <td>
        <div class="main">
            ...
        </div>
      </td>
   </tr>
</tbody>

Notice that my tbody only has one tr and td.  Because we only use the tbody here for structure, not content, it’s not necessary to use it like a traditional table, with several rows and cells.

!important CSS

Most of the important CSS necessary for repeating the headers can be applied inside @media print.  This is handy because you can still format all your HTML for the screen while making things look nice when they’re printed.

display:table-header-group;

However, to insure the thead and tfoot repeat, we need to change their display: property.  I put this with all the regular CSS, not within @media print.

/*CSS*/
thead.report-header {
   display: table-header-group;
}

tfoot.report-footer {
   display:table-footer-group;
}

See how using the classes on thead and tfoot come in handy?  Really, the display:table-*-group is just a back-up method of forcing the browser to treat the thead and tfoot like theads and tfoots.  They’re not necessary, but very helpful.

page-break-after: always;

The last thing we needed to do was to make sure the printed pages broke after each Report, keeping all the pages for a specific report together and so each Report could be separated from the others.

Using the .report-container class on our Report table, we can add page-break-after: always to cause the printed pages to break after each Report is printed.

/* Css */

tabel.report-container {
    page-break-after: always;
}

Page breaks can be applied to almost anything, and you can specify -before or -after.  They seem like they would be really handy in more print-first applications.

Here’s where the page would break when printing:

Which results in our desired layout when printing:

Putting it all together

Here’s all the table layout and CSS code for the repeating headers.

HTML Table Structure

<table class="report-container">
   <thead class="report-header">
     <tr>
        <th class="report-header-cell">
           <div class="header-info">
            ...
           </div>
         </th>
      </tr>
    </thead>
    <tfoot class="report-footer">
      <tr>
         <td class="report-footer-cell">
           <div class="footer-info">
           ...
           </div>
          </td>
      </tr>
    </tfoot>
    <tbody class="report-content">
      <tr>
         <td class="report-content-cell">
            <div class="main">
            ...
            </div>
          </td>
       </tr>
     </tbody>
</table>

CSS

table.report-container {
    page-break-after:always;
}
thead.report-header {
    display:table-header-group;
}
tfoot.report-footer {
    display:table-footer-group;
} 

What do you think?

Have you ever tried any other method of repeating content on printed pages from the browser? Let me know!

39 Comments

  • Hi Jessica,

    I’m working with this tecnique for months now and have had many troubles. Recentely, with the last Firefox version, it started to overlap some headers with the content.
    Are you experiencing this too?

    • Yeah….sooo….It’s super not 100% all the time, which sucks.

      One thing I did discover after posting this was there is a limit to the height of the thead of 251px. I’m not sure why this is or what determines that height.
      https://stackoverflow.com/questions/2493638/are-there-any-thead-limitations-that-make-it-not-print-on-each-page-in-firefox We when started testing in FF, that made a huge difference.

      ALSO: I’ve found that yeah, with FF, is very hit and miss in its application. Even with theads smaller than 251px, sometimes it would overlap. I could never determine exactly what was going on when I ran into that. Sometimes, it was the print margins being set by the print driver instead of the browser. Sometimes it was a tbody height issue with the CSS.

      We ended up going a different route to get this to work utilizing JQuery. I didn’t update this post because it seemed that in limited, simple cases, this still might work.

      I wish CSS3 would hurry up and be implemented across the board. It would make this issue soooo much easier to deal with!

      • check this out Jessica

        /* THE FOLLOWING CSS IS REQUIRED AND SHOULD NOT BE MODIFIED. */
        div.fauxRow {
        display: inline-block;
        vertical-align: top;
        width: 100%;
        page-break-inside: avoid;
        }
        table.fauxRow {
        border-spacing: 0;
        }
        table.fauxRow > tbody > tr > td {
        padding: 0;
        overflow: hidden;
        }
        table.fauxRow > tbody > tr > td > table.print {
        display: inline-table;
        vertical-align: top;
        }
        table.fauxRow > tbody > tr > td > table.print > caption {
        caption-side: top;
        }
        .noBreak {
        float: right;
        width: 100%;
        visibility: hidden;
        }
        .noBreak:before,
        .noBreak:after {
        display: block;
        content: “”;
        }
        .noBreak:after {
        margin-top: -594mm;
        }
        .noBreak > div {
        display: inline-block;
        vertical-align: top;
        width: 100%;
        page-break-inside: avoid;
        }
        /*table.print > thead {white-space: nowrap;}*/ /* Uncomment if line-wrapping causes problems. */
        table.print > tbody > tr {
        page-break-inside: avoid;
        }
        table.print > tbody > .metricsRow > td {
        border-top: none !important;
        }

        /* THE FOLLOWING CSS IS REQUIRED, but the values may be adjusted. */
        /* NOTE: All size values that can affect an element’s height should use the
        px unit! */
        table.fauxRow,
        table.print {
        font-size: 16px;
        line-height: 20px;
        }
        /* THE FOLLOWING CSS IS OPTIONAL. */
        body {
        counter-reset: t1;
        } /* Delete to remove row numbers. */
        .noBreak .t1 > tbody > tr > :first-child:before {
        counter-increment: none;
        } /*
        Delete to remove row numbers. */
        .t1 > tbody > tr > :first-child:before {
        /* Delete to remove row numbers. */
        display: block;
        text-align: right;
        counter-increment: t1 1;
        content: counter(t1);
        }
        table.fauxRow,
        table.print {
        font-family: Tahoma, Verdana, Georgia; /* Try to use fonts that don’t get
        bigger when printed. */
        width: 100%;
        }
        table.print {
        border-spacing: 0;
        }
        table.print > * > tr > * {
        padding: 0 5px 0 5px;
        }
        table.print > thead {
        vertical-align: bottom;
        }

        table.print > tbody {
        vertical-align: top;
        }
        table.print > caption {
        font-weight: bold;
        }


        Print-Friendly Table

        content
        content
        content
        content
        content
        content
        content
        content
        content
        contentcontent
        content
        content
        content
        content
        content
        content
        content
        content
        content
        content

        data
        Multiplelines ofdata
        data

        (function() {
        // THIS FUNCTION IS NOT REQUIRED. It just adds table rows for testing purposes.
        var rowCount = 100,
        tbod = document.querySelector(‘table.print > tbody’),
        row = tbod.rows[0];
        for (; –rowCount; tbod.appendChild(row.cloneNode(true)));
        })();

        (function() {
        // THIS FUNCTION IS REQUIRED.
        if (/Firefox|MSIE |Trident/i.test(navigator.userAgent))
        var formatForPrint = function(table) {
        var noBreak = document.createElement(‘div’),
        noBreakTable = noBreak
        .appendChild(document.createElement(‘div’))
        .appendChild(table.cloneNode()),
        tableParent = table.parentNode,
        tableParts = table.children,
        partCount = tableParts.length,
        partNum = 0,
        cell = table.querySelector(‘tbody > tr > td’);
        noBreak.className = ‘noBreak’;
        for (; partNum tr > td’);
        if (cell) {
        var topFauxRow = document.createElement(‘table’),
        fauxRowTable = topFauxRow
        .insertRow(0)
        .insertCell(0)
        .appendChild(table.cloneNode()),
        colgroup = fauxRowTable.appendChild(
        document.createElement(‘colgroup’)
        ),
        headerHider = document.createElement(‘div’),
        metricsRow = document.createElement(‘tr’),
        cells = cell.parentNode.cells,
        cellNum = cells.length,
        colCount = 0,
        tbods = table.tBodies,
        tbodCount = tbods.length,
        tbodNum = 0,
        tbod = tbods[0];
        for (; cellNum–; colCount += cells[cellNum].colSpan);
        for (
        cellNum = colCount;
        cellNum–;
        metricsRow.appendChild(document.createElement(‘td’)).style.padding = 0
        );
        cells = metricsRow.cells;
        tbod.insertBefore(metricsRow, tbod.firstChild);
        for (
        ;
        ++cellNum < colCount;
        colgroup.appendChild(document.createElement('col')).style.width =
        cells[cellNum].offsetWidth + 'px'
        );
        var borderWidth = metricsRow.offsetHeight;
        metricsRow.className = 'metricsRow';
        borderWidth -= metricsRow.offsetHeight;
        tbod.removeChild(metricsRow);
        tableParent.insertBefore(topFauxRow, table).className = 'fauxRow';
        if (table.tHead) fauxRowTable.appendChild(table.tHead);
        var fauxRow = topFauxRow.cloneNode(true),
        fauxRowCell = fauxRow.rows[0].cells[0];
        fauxRowCell.insertBefore(
        headerHider,
        fauxRowCell.firstChild
        ).style.marginBottom =
        -fauxRowTable.offsetHeight – borderWidth + 'px';
        if (table.caption)
        fauxRowTable.insertBefore(table.caption, fauxRowTable.firstChild);
        if (tbod.rows[0])
        fauxRowTable.appendChild(tbod.cloneNode()).appendChild(tbod.rows[0]);
        for (; tbodNum < tbodCount; tbodNum++) {
        tbod = tbods[tbodNum];
        rows = tbod.rows;
        for (
        ;
        rows[0];
        tableParent
        .insertBefore(fauxRow.cloneNode(true), table)
        .rows[0].cells[0].children[1].appendChild(tbod.cloneNode())
        .appendChild(rows[0])
        );
        }
        tableParent.removeChild(table);
        } else
        tableParent
        .insertBefore(document.createElement('div'), table)
        .appendChild(table).parentNode.className =
        'fauxRow';
        };
        var tables = document.body.querySelectorAll('table.print'),
        tableNum = tables.length;
        for (; tableNum–; formatForPrint(tables[tableNum]));
        })();

  • Do you know _why_ tfoot has to be before tbody (as if it is a html 4 document) instead of after it (as if it were a html 5 document)?
    I mean, what problems resulted in which browser otherwise?

  • Loved the solution! Works better than all the others I found so far, for my case, where using stylesheets wasn’t an option, and only inline styling works.

  • Thx! It’s really helphul.
    I found only method with `position:fixed` but with header it break second page content.
    With this i can use header.
    Thx!Thx!Thx!

  • Is it possible to do this by using pure divs? I tried “dislpay:table-header-group;” on the div that works as the table header but it doesn’t work. I’m using PhantomJS (QtWebKit engine).
    Thank you

    • As far as I can tell from my research and testing, no. :/ The problem, for what I can tell, is communicating the content to the print dialog from the page after the print dialog as been engaged.

      One thing I didn’t test was somehow dynancially generating content using js and a print.css, but even as I’m typing this, I’m not confident in that working.

      All the modern solutions I found was generating a PDF *before* triggering the print dialog. There are a few js plugins, free and paid, that can help to do that. We couldn’t explore those options because of our database limitations, but I’d be interested to see how that works.

  • Amazing, it works, thank you so muchhhhhhhhhh… !!!!
    Wish you all the best in your entire life 😀

  • Hi Jessica-

    I am having to do almost identical work, and really appreciate this post! The one difference in overall “goals” is that I want the to always display at the bottom of the page when printed, regardless of where the content ends.

    Do you know of any sneaky ways to achieve this? I only need it to work in IE (again with the 1996) so even a way that’s browser-limited would be so appreciated..
    Thanks so much!

    • > The one difference in overall “goals” is that I want the to always display at the bottom of the page when printed, regardless of where the content ends.

      If you meant “footer” to display in the bottom regardless of where the content ends, I agree with you. This is something I need also. The last page sometimes doesn’t have full height so the footer does not display in the bottom.

    • Hi Ben,

      Did you figure out how to print page number in a footer? I am trying to do this now and I haven’t found a good solution.

  • I want to use it in chrome and yandex browser. I tried this in IE and Firefox but is not work IE and firefox also. I using div for header, footer and for content using table.

  • Hi, Jessica!

    I don’t know if you can help me, but I’ve found this solution (using `display`, not the actual table, and not here) sometime ago. Them I has a happy person, printing my stories like a book from the site (http://meak-stories.com) (it is in portuguese).

    Then I showed it to someone that noticed that the headers and footers were being printed over the text. I’ve only tested on Firefox. On Chrome and Edge, it is not working…

    The css:
    https://github.com/darakeon/stories-site/blob/master/Site/Presentation/Assets/css/print_Print.css

    The html (header/footer):
    https://github.com/darakeon/stories-site/blob/master/Site/Presentation/Views/Season/Partial/PrintHeaderAndFooter.cshtml

    I really don’t know why it’s this way, almost giving up, at least giving up for today.

    Thanks, even if you read this and not answer.

  • If you’re making PDF’s server side with wkhtmltopdf the method above does NOT work.

    Instead i think you use –header-html and –footer-html options in wkhtmltopdf. Not all version of wkhtmltopdf support these options, you may have to replace/update your wkhtmltopdf.

  • This post helped me a lot!
    I’m still having the problem to always print the footer at the bottom of the page, though. If anyone could help, I’d appreciate it.

  • Hi Jessica,

    Your solution with table is quite nice, but on all pages (beyond first) header is on content 🙁 Do U have any suggestion how to correct it ?

  • Hi Jessica,

    damn u saved me from a lot of trouble! Works like a charm 🙂

    Thank you !

  • Great explanation. It works very well.

    I needed to position the footer at the bottom of the page, even for the last page containing a footer. I added this script after the table. It adds padding to the final div in the table to make it the size of a page less the size of the footer.

    var ele = document.querySelector(“tbody> tr > td :last-child > div”)
    ele.style.paddingBottom = window.innerHeight – (
    ele.getBoundingClientRect().height +
    document.querySelector(“tfoot.report-footer”).getBoundingClientRect().height
    ) % window.innerHeight + “px”

  • Hi there,
    It works like a charm! Thanks a million!
    There is only one problem that I couldn’t fix.
    I have a table inside `div.main` and the length of this data could be different. So, sometimes I have only one page and sometimes more.
    I need to integrate pagination and show it somewhere on top-right of the page.
    But counter-increment doesn’t work.
    Is there any workaround that we can integrate pagination to the output like as follow:
    Page 1 of 3
    Page 2 of 3
    Page 3 of 3

  • A little late to the game, but I saw some people comment asking about keeping the footer at the bottom of the page. I found when I put style=”position:fixed; bottom:0px; in the footer div kept my footer at the bottom of the page. I’ll write the full example below. Hope this helps!

    footer

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© Jessica Schillinger  |  Powered by Wordpress