47

I'd like to have my table's headers repeated for every printed page, but it seems Google Chrome doesn't support the <thead> tag well...is there a way around this? I'm using Google Chrome v13.0.782.215.

The table code is very straightforward...nothing fancy:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <style type="text/css" media="all">
           @page {
              size: landscape;
              margin-top: 0;
              margin-bottom: 1cm;
              margin-left: 0;
              margin-right: 0;
           }
           table {
               border: .02em solid #666; border-collapse:collapse; 
               width:100%; 
           }
           td, th {
               border: .02em solid #666; font-size:12px; line-height: 12px; 
               vertical-align:middle; padding:5px; font-family:"Arial";
           }
           th { text-align:left; font-size:12px; font-weight:bold; }
           h2 { margin-bottom: 0; }
       </style>
   </head>
   <body>
   <h2>Page Title</h2>
   <table>
       <thead>
           <tr class="row1">
               <th><strong>Heading 1</strong></th>
               <th><strong>Heading 2</strong></th>
               <th><strong>Heading 3</strong></th>
               <th><strong>Heading 4</strong></th>
               <th><strong>Heading 5</strong></th>
           </tr>
       </thead>
       <tbody>
           <tr class="row2">
              <td width="30">...</td>
              <td width="30">...</td>
              <td width="90">....</td>
              <td width="190">...</td>
              <td width="420">...</td>
           </tr>
           <tr class="row1">
              <td width="30">...</td>
              <td width="30">...</td>
              <td width="90">....</td>
              <td width="190">...</td>
              <td width="420">...</td>
           </tr>
           ....
       </tbody>
   </table>
   </body>
</html>

Any insight into this is welcome.

Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
Stephen M
  • 809
  • 1
  • 10
  • 21
  • You could check the Chrome bug tracker to see if this is a bug – Yi Jiang Aug 28 '11 at 03:46
  • 5
    @Yi Jiang: I did and it seems it's a long standing issue that has never been addressed...it's really frustrating – Stephen M Aug 29 '11 at 15:34
  • 1
    If you care about this bug getting fixed in Chrome, please use the link below to go to the Bug Report and "Star" it by clicking the star at the top left corner. – Dmitri Sunshine Dec 06 '12 at 01:21

7 Answers7

53

UPDATE 2017-03-22: Repeating table headers have finally been implemented in Chrome! (Actually, I think they were implemented some time ago.) That means you probably don't need this solution anymore; just put your column headers in a <thead> tag and you should be all set. Use the solution below only if:

  • you encounter show-stopping bugs in Chrome's implementation,
  • you need the "bonus features", or
  • you need to support some oddball browser that still doesn't support repeating headers.

SOLUTION (obsolete)

The code below demonstrates the best method I've found for multi-page table printing. It has the following features:

  • Column headers repeat on each page
  • No need to worry about paper size or how many rows will fit-- the browser handles everything automatically
  • Page breaks occur only between rows
  • Cell borders are always fully closed
  • If a page break occurs near the top of the table, it won't leave behind an orphaned caption or column headers with no data attached (a problem which isn't limited to just Chrome)
  • Works in Chrome! (and other Webkit-based browsers like Safari and Opera)

... and the following known limitations:

  • Only supports 1 <thead> (which is apparently the most you're allowed to have anyway)
  • Doesn't support <tfoot> (though Chrome-compatible running footers are technically possible)
  • Only supports top-aligned <caption>
  • Table can't have top or bottom margin; to add white space above or below the table, insert an empty div and set a bottom margin on it
  • Any CSS size values that affect height (including border-width and line-height) must be in px
  • Column widths can't be set by applying width values to individual table cells; you should either let cell content automatically determine column width, or use <col>s to set specific widths if needed

  • Table can't (easily) be changed dynamically after the JS has run

THE CODE

<!DOCTYPE html>
<html>
  <body>
    <table class="print t1"> <!-- Delete "t1" class to remove row numbers. -->
      <caption>Print-Friendly Table</caption>
      <thead>
        <tr>
          <th></th>
          <th>Column Header</th>
          <th>Column Header</th>
          <th>Multi-Line<br/>Column<br/>Header</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td></td>
          <td>data</td>
          <td>Multiple<br/>lines of<br/>data</td>
          <td>data</td>
        </tr>
      </tbody>
    </table>
  </body>
</html>

<style>
  /* 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 > 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. */
      margin: 0 auto 0 auto; /* Delete if you don't want table to be centered. */
    }
    table.print {border-spacing: 0;}
    table.print > * > tr > * {
      border-right: 2px solid black;
      border-bottom: 2px solid black;
      padding: 0 5px 0 5px;
    }
    table.print > * > :first-child > * {border-top: 2px solid black;}
    table.print > thead ~ * > :first-child > *, table.print > tbody ~ * > :first-child > * {border-top: none;}
    table.print > * > tr > :first-child {border-left: 2px solid black;}
    table.print > thead {vertical-align: bottom;}
    table.print > thead > .borderRow > th {border-bottom: none;}
    table.print > tbody {vertical-align: top;}
    table.print > caption {font-weight: bold;}
</style>

<script>
  (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 < partCount; partNum++) {
          if(!/tbody/i.test(tableParts[partNum].tagName))
            noBreakTable.appendChild(tableParts[partNum].cloneNode(true));
        }
        if(cell) {
          noBreakTable.appendChild(cell.parentNode.parentNode.cloneNode()).appendChild(cell.parentNode.cloneNode(true));
          if(!table.tHead) {
            var borderRow = document.createElement("tr");
            borderRow.appendChild(document.createElement("th")).colSpan="1000";
            borderRow.className = "borderRow";
            table.insertBefore(document.createElement("thead"), table.tBodies[0]).appendChild(borderRow);
          }
        }
        tableParent.insertBefore(document.createElement("div"), table).style.paddingTop = ".009px";
        tableParent.insertBefore(noBreak, table);
      };
    else
      var formatForPrint = function(table) {
        var tableParent = table.parentNode
          , cell = table.querySelector("tbody > 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]));
  })();
</script>

HOW IT WORKS (If you don't care, read no further; everything you need is above.)

Per @Kingsolmn's request, below is an explanation of how this solution works. It doesn't cover the JavaScript, which isn't strictly required (though it does make this technique much easier to use). Instead, it focuses on the generated HTML structures and associated CSS, which is where the real magic happens.

Here's the table we'll be working with:

<table>
  <tr><th>ColumnA</th><th>ColumnB</th></tr>
  <tr><td>row1</td><td>row1</td></tr>
  <tr><td>row2</td><td>row2</td></tr>
  <tr><td>row3</td><td>row3</td></tr>
</table>

(To save space, I gave it only 3 data rows; obviously, a multipage table would usually have more)

The first thing we need to do is split the table into a series of smaller tables, each with its own copy of the column headers. I call these smaller tables fauxRows.

<table> <!-- fauxRow -->
  <tr><th>ColumnA</th><th>ColumnB</th></tr>
  <tr><td>row1</td><td>row1</td></tr>
</table>

<table> <!-- fauxRow -->
  <tr><th>ColumnA</th><th>ColumnB</th></tr>
  <tr><td>row2</td><td>row2</td></tr>
</table>

<table> <!-- fauxRow -->
  <tr><th>ColumnA</th><th>ColumnB</th></tr>
  <tr><td>row3</td><td>row3</td></tr>
</table>

The fauxRows are essentially clones of the original table, but with only one data row apiece. (If your table has a caption, though, only the top fauxRow should include it.)

Next we need to make the fauxRows unbreakable. What does that mean? (Pay attention-- this is probably the most important concept in page break handling.) "Unbreakable" is the term I use to describe a block of content that can't be split between two pages*. When a page break occurs within the space occupied by such a block, the whole block moves to the next page. (Note that I'm using the word "block" informally here; I'm not referring specifically to block level elements.) This behavior has an interesting side-effect that we'll make use of later on: it can expose content that was initially hidden due to layering or overflow.

We can make the fauxRows unbreakable by applying either of the following CSS declarations:

  • page-break-inside: avoid;
  • display: inline-table;

I usually use both, because the first is made for this purpose and the second works in older/noncompliant browsers. In this case, though, for simplicity's sake, I'll stick to the page-break property. Note that you will not see any change in the table's appearance after adding this property.

<table style="page-break-inside: avoid;"> <!-- fauxRow -->
      <tr><th>ColumnA</th><th>ColumnB</th></tr>
      <tr><td>row1</td><td>row1</td></tr>
    </table>
    
    <table style="page-break-inside: avoid;"> <!-- fauxRow -->
      <tr><th>ColumnA</th><th>ColumnB</th></tr>
      <tr><td>row2</td><td>row2</td></tr>
    </table>
    
    <table style="page-break-inside: avoid;"> <!-- fauxRow -->
      <tr><th>ColumnA</th><th>ColumnB</th></tr>
      <tr><td>row3</td><td>row3</td></tr>
    </table>

Now that the fauxRows are unbreakable, if a page break occurs within a data row, it will shift to the next page along with its attached header row. So the next page will always have column headers at the top, which is our goal. But the table looks very strange now with all the extra header rows. To make it look like a normal table again, we need to hide the extra headers in such a way that they'll appear only when needed.

What we're going to do is put each fauxRow in a container element with overflow: hidden; and then shift it upward so that the headers get clipped by the top of the container. This will also move the data rows back together so that they appear contiguous.

Your first instinct might be to use divs for the containers, but we're going to use the cells of a parent table instead. I'll explain why later, but for now, let's just add the code. (Once again, this will not affect the table's appearance.)

table {
  border-spacing: 0;
  line-height: 20px;
}
th, td {
  padding-top: 0;
  padding-bottom: 0;
}
<table> <!-- parent table -->
  <tr>
    <td style="overflow: hidden;">

      <table style="page-break-inside: avoid;"> <!-- fauxRow -->
        <tr><th>ColumnA</th><th>ColumnB</th></tr>
        <tr><td>row1</td><td>row1</td></tr>
      </table>

    </td>
  </tr>
  <tr>
    <td style="overflow: hidden;">

      <table style="page-break-inside: avoid;"> <!-- fauxRow -->
        <tr><th>ColumnA</th><th>ColumnB</th></tr>
        <tr><td>row2</td><td>row2</td></tr>
      </table>

    </td>
  </tr>
  <tr>
    <td style="overflow: hidden;">

      <table style="page-break-inside: avoid;"> <!-- fauxRow -->
        <tr><th>ColumnA</th><th>ColumnB</th></tr>
        <tr><td>row3</td><td>row3</td></tr>
      </table>

    </td>
  </tr>
</table>

Notice the CSS above the table markup. I added it for two reasons: first, it prevents the parent table from adding white space between the fauxRows; second, it makes the header height predictable, which is necessary since we're not using JavaScript to calculate it dynamically.

Now we just need to shift the fauxRows upward, which we'll do with negative margins. But it's not as simple as you might think. If we add a negative margin directly to a fauxRow, it will remain in effect when the fauxRow gets bumped to the next page, causing the headers to get clipped by the top of the page. We need a way to leave the negative margin behind.

To accomplish this, we'll insert an empty div above each fauxRow after the first and add the negative margin to it. (The first fauxRow is skipped because its headers should always be visible.) Since the margin is on a separate element, it won't follow the fauxRow to the next page, and the headers won't be clipped. I call these empty divs headerHiders.

table {
  border-spacing: 0;
  line-height: 20px;
}
th, td {
  padding-top: 0;
  padding-bottom: 0;
}
<table> <!-- parent table -->
  <tr>
    <td style="overflow: hidden;">

      <table style="page-break-inside: avoid;"> <!-- fauxRow -->
        <tr><th>ColumnA</th><th>ColumnB</th></tr>
        <tr><td>row1</td><td>row1</td></tr>
      </table>

    </td>
  </tr>
  <tr>
    <td style="overflow: hidden;">

      <div style="margin-bottom: -20px;"></div> <!-- headerHider -->

      <table style="page-break-inside: avoid;"> <!-- fauxRow -->
        <tr><th>ColumnA</th><th>ColumnB</th></tr>
        <tr><td>row2</td><td>row2</td></tr>
      </table>

    </td>
  </tr>
  <tr>
    <td style="overflow: hidden;">

      <div style="margin-bottom: -20px;"></div> <!-- headerHider -->

      <table style="page-break-inside: avoid;"> <!-- fauxRow -->
        <tr><th>ColumnA</th><th>ColumnB</th></tr>
        <tr><td>row3</td><td>row3</td></tr>
      </table>

    </td>
  </tr>
</table>

That's it, we're done! On screen, the table should now look normal, with only one set of column headers at the top. In print, it should now have running headers.

If you were wondering why we used a parent table instead of a bunch of container divs, it's because Chrome/webkit has a bug that causes a div-enclosed unbreakable block to carry its container to the next page with it. Since the headerHider is also in the container, it won't get left behind like it's supposed to, which leads to clipped headers. This bug only happens if the unbreakable block is the topmost element in the div with a non-zero height.

I did discover a workaround while writing this tutorial: you just have to explicitly set height: 0; on the headerHider and give it an empty child div with a non-zero height. Then you can use a div container. I still prefer to use a parent table, though, because it has been more thoroughly tested, and it salvages the semantics to some extent by tying the fauxRows back together into a single table.

EDIT: I just realized that the JavaScript-generated markup is slightly different in that it puts each fauxRow in a separate container table, and assigns the "fauxRow" className to it (the container). This would be required for footer support, which I intended to add someday but never did. If I were to update the JS, I might consider switching to div containers since my semantic justification for using a table doesn't apply.

* There is one situation in which an unbreakable block can be split between two pages: when it exceeds the height of the printable area. You should try to avoid this scenario; you're essentially asking the browser to do the impossible, and it can have some very strange effects on the output.

Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
DoctorDestructo
  • 4,166
  • 25
  • 43
  • @DoctorDestructo It does not seem to work with nested tables. If this is excepected, I think it should be added in the known limitations, otherwise please indicate how to make it work with nested tables – Eric Lemanissier Dec 01 '14 at 13:29
  • @DoctorDestructo I was talking of nesting a print table in another print table – Eric Lemanissier Dec 01 '14 at 14:26
  • Nesting print table is very useful, for example if you want to set a global header to appear on each page, and you then have all the body of the document which can contain also print table, like in the following example : http://pastebin.com/w0A4LDz9 – Eric Lemanissier Dec 01 '14 at 15:04
  • @DoctorDestructo the printed result of the above pastebin is the following when printed in IE: http://pbrd.co/1tv7qrn http://pbrd.co/1tv7Iyf http://pbrd.co/1tv7Td0 – Eric Lemanissier Dec 01 '14 at 15:26
  • My table only allows page breaks between rows. That's by design. If you're using the table for its intended purpose (i.e. displaying tabular data) it is rarely desirable to allow page breaks within rows. Therefor, I view the unbreakable rows as a feature rather than a limitation. It does mean that you can't use this table for general purpose page headers, but that's not what tables were designed for anyway. As far as I know, there is no good way to create repeating page headers in webkit-based browsers at this time. – DoctorDestructo Dec 01 '14 at 17:46
  • 1
    @DoctorDestructo i tried to implement this solution, it really avoided the page breaking inside a row, but i ended up with the columns all messed up, like this image : http://s11.postimg.org/uyhoktapv/error.jpg Can you help me to fix that ? Thanks in advance ! – delphirules Nov 17 '15 at 12:46
  • 2
    @delphirules Make sure no width values are being applied to any `` or `` elements in the table. There are only two valid ways to set column width: 1) let the cell content set it automatically, or 2) use [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/col) elements. If that's not the problem, then I'd probably have to see your table html (prior to running the required js function) to diagnose it. – DoctorDestructo Nov 17 '15 at 19:07
  • @DoctorDestructo it seems the problem is on 'min-width' property of my table element ; if i remove this, the table renders ok. Another question, please : the problem of the page-break row is solved, BUT the first row of the next page does not respect the margin-top and as result, is render too close of the page bottom. Is there a way to solve this ? Thank you very much for your attention. – delphirules Nov 18 '15 at 12:53
  • 1
    @delphirules If your table has top padding applied to it, that could cause the behavior you describe. The best workaround would be to get rid of the top padding and instead add a bottom margin to the element above the table. (In fact, when it comes to printing, it's usually best to use bottom margins for vertical white space). – DoctorDestructo Nov 18 '15 at 17:46
  • @DoctorDestructo if you have, or ever find the time for, an explanation of how your wizardry works, I'd love to see it in a shareable blog format or something! **>> And thank you for this!! <<** – Kingsolmn Apr 08 '16 at 23:56
  • 1
    @Kingsolmn yw! Here's a simple explanation of how it works: The js splits your table into a series of one-row subtables, each with its own set of column headers. A negative margin partially retracts each subtable into its container, hiding the headers. The margin is implemented in such a way that it doesn't carry over if a subtable gets bumped to the next page by a page break. So, the first subtable on each page will have visible headers. – DoctorDestructo Apr 14 '16 at 21:01
  • 1
    @Kingsolmn I've thought about adding more detailed explanations to this post. I've even thought about adding footer support. But, amazingly, Chrome's bug tracker says someone's actually trying to [fix this problem](https://bugs.chromium.org/p/chromium/issues/detail?id=24826&q=print%20thead&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified). If they succeed, I doubt this question will be getting any more views. – DoctorDestructo Apr 14 '16 at 21:27
  • @DoctorDestructo You'd be surprised how an answer will get attention even after it's not really relevant any more, I've got an answer in Android from pre-HoneyComb days that still gets attention! – Kingsolmn Apr 15 '16 at 13:59
  • @Kingsolmn Ok, you win. I added a detailed explanation at the bottom. See if you have the stamina to read it all the way through :) – DoctorDestructo Apr 29 '16 at 02:52
  • @DoctorDestructo I'm not sure what I'm doing wrong. I've used your code (plugin) before and it worked great (though it's never seemed to actually respect avoiding page-breaks within rows in Chrome as described), nevertheless, now when I try to use it, it repeats the `` on each and every row, instead of page. I've assigned the class 'print' to the table, and it seems set up just as I've done this before, yet after trouble-shooting for two days, I can't seem to find out what I'm doing wrong. Any ideas of what would cause a header on every single row? I've got the table's padding set to 0. – VoidKing May 16 '16 at 17:17
  • @VoidKing Looks like I screwed up the "known limitations" bullet points. It's actually *margins* that cause problems, not padding. So, make sure your table has no top margin. However, if you're seeing individual rows being cut in two by page breaks, that indicates a bigger problem, because this technique only works if the rows are unbreakable. – DoctorDestructo May 16 '16 at 19:19
  • @DoctorDestructo Thanks for the reply, and THANK YOU for clearing up the margin vs. padding mistake. You may want to correct that, though, as I did have 5px top margin set. However, that wasn't the main thing throwing me off. Now, I know I should have seen this sooner, but I don't really remember setting this rule, at all, but I found where I had set position relative and, you guessed it, 'top: 30px' to the table. This was the issue, though once corrected, I saw where each row was offset by the 5px I had for the top margin. Fixing these things, it worked great as I remember. – VoidKing May 16 '16 at 20:05
  • @DoctorDestructo For this code you've provided you really do deserve about 6 trillion more upvotes. For a leading web-browser like Chrome, it really is a shame that they can't seem to - in the 6 years they've known about it - fix this bug (and other bugs in Chrome based in printing, like not recognizing `page-break-inside: avoid;` for instance). However, without this code that you've obviously worked hard on, I simply don't know what the world would do. I, for instance, am forced to use Chrome for the limitations of other browsers. Without this, I'd simply be dead in the water. THANK YOU! – VoidKing May 16 '16 at 20:08
  • @DoctorDestructo Any idea what may cause some of the normal table rows to show what seems like an solid 1px black bottom broder (to only 'some' of the table rows)? I've removed all of the margins, and have exhausted all of what I thought it might be. – VoidKing May 16 '16 at 20:30
  • 1
    @VoidKing Thanks! I corrected the aforementioned bullet point, but, as you've discovered, the bullet points don't actually cover all the ways CSS can mess things up. Since the JavaScript radically changes the table's HTML structure (essentially turning every row into a mini clone of the table), complex selectors can easily end up applying to the wrong elements. For best results, I'd recommend setting CSS classes directly on the table parts that you want to style (as opposed to using combinators to target descendant or sibling elements). – DoctorDestructo May 16 '16 at 22:44
  • 1
    @voidKing Wrt the Chrome bug, it's actually a [WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=17205) that was first reported more than 8 years ago, before Chrome was even released. Unfortunately, it's also technically not a bug since the [table spec](https://bugs.webkit.org/show_bug.cgi?id=17205) never made repeating headers/footers mandatory. I figured the WebKit devs gave themselves a pass because of that and had no intention of ever fixing it. A Chromium dev did [give us some hope](http://stackoverflow.com/a/36298205/2759272) earlier this year, but it's starting to fade. – DoctorDestructo May 16 '16 at 23:25
  • 1
    @VoidKing Getting back to the issue of CSS combinators, I should probably be a little more specific on when to avoid them. They'll probably work as expected most of the time; it's mainly when you use them to target sibling rows that problems occur. So avoid selectors like this: `table.print > tbody > tr + tr`, or this: `tr ~ tr`. Also avoid using pseudo-classes like `:first-child`, `:last-child`, `:nth-child`, etc. to target specific rows. In general, if you want to style a row differently, it's best to give it its own class. – DoctorDestructo May 17 '16 at 00:38
  • @DoctorDestructo Okay, duly noted. Thank you so much, again. Now, I have but one task left to accomplish (I swear, my company thinks that what I do is magic...), and that is to add a "footer" to each page (the scary part is that this has to be in addition to using this plugin, at the same time). I don't know how much trouble I'll have doing this, but hopefully some type of set up is possible to get this done, though I may need to call on your help again if I need to adjust the overall measured height of the page in your plugin, offset by however tall the footer is. – VoidKing May 17 '16 at 13:39
  • @VoidKing If you need repeating table footers, [this post](http://stackoverflow.com/a/28777105/2759272) shows how it's done. If you really did mean _page_ footers, though, my advice would be to give up on HTML printing and find a good PDF converter. If you ever do find a way to do page footers in Chrome _without_ going the PDF route, though, please share it with me and the world. – DoctorDestructo May 17 '16 at 16:20
  • @DoctorDestructo Well, I'm basically having to calculate and count the amount of rows (with fixed height) and just do this specific to my cause (hardly helpful to you or the rest of the world). If I had known I had to do this, I could have avoided your plugin altogether (since I, basically have to write them on the fly with C# and row counts), EXCEPT, I've never found a way to not have rows split in Chrome. Is how you are doing this simply by inserting copies of tables and the fact that Chrome knows better than to break in the middle of tables for page breaks? – VoidKing May 18 '16 at 20:19
  • 1
    @VoidKing We're getting a bit off topic here, but I get the impression, based on the messages at the bottom of [this code review](https://codereview.chromium.org/1602773005/#ps300001), that Rob Hogan (our hero Chromium dev who's working toward repeating headers) has implemented `page-break-inside: avoid;` for table rows! Still doesn't work in my Chrome, but maybe it will soon. You got the gist of how I did it, except that Chrome has to be "told" (via CSS) not to break the table copies. I use the following CSS to accomplish this: `display: inline-table; page-break-inside: avoid;`. – DoctorDestructo May 19 '16 at 01:00
  • @DoctorDestructo Okay, thank you, and sorry for getting a little off topic. So, Chrome recognizes `page-break-inside: avoid;` for elements with `display: inline-table;`? If so, that's good to know until that Chrome developer accomplishes this task (which has been such a long time coming). Thanks for all your help! – VoidKing May 19 '16 at 16:19
  • @VoidKing You could actually use either declaration by itself; both prevent page breaks equally well. In the case of `display: inline-table;`, though, it's just a side-effect and not the intended purpose. The only reason I continue to use it is that I still occasionally see reports that `page-break-inside: avoid;` doesn't work on tables in Safari. – DoctorDestructo May 19 '16 at 17:45
40

I believe this is a bug in Chrome.

Arif Uddin
  • 416
  • 6
  • 3
4

now it's possible to print in chrome using jQuery.... please try this code (i'm sorry forget who's the creator of this code before i modified - and my english language is not good :D hehehe)

            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
            <html>
            <head>
                <title>DOCUMENT TITLE</title>
                <link rel="stylesheet" type="text/css" href="assets/css/bootstrap.css"/>
                <style type="text/css">
                    @media print{
                        table { page-break-after:auto;}
                        tr    { page-break-inside:avoid;}
                        td    { page-break-inside:auto;}
                        thead { display:table-header-group }

                        .row-fluid [class*="span"] {
                          min-height: 20px;
                        }
                    }

                    @page { 
                        margin-top: 1cm;
                        margin-right: 1cm;
                        margin-bottom:2cm;
                        margin-left: 2cm;';
                        size:portrait;
                        /*
                        size:landscape;
                        -webkit-transform: rotate(-90deg); -moz-transform:rotate(-90deg);
                        filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
                        */

                    };
                </style>
            </head>
            <body>
                <div id="print-header-wrapper">
                    <div class="row-fluid">HEADER TITLE 1</div>
                    <div class="row-fluid">HEADER TITLE 2</div>
                </div>
                <div class="row-fluid" id="print-body-wrapper">
                    <table class="table" id="table_data">
                        <thead>
                            <tr><th>TH 1</th><th>TH 2</th></tr>
                        </thead>
                        <tbody>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                            <tr><td>TD 1</td><td>TD 2</td></tr>
                        </tbody>
                    </table>
                    <div id="lastDataTable"></div>
                </div>
                <script type="text/javascript">
                    jQuery(document).ready(function()
                    {
                        var printHeader = $('#print-header-wrapper').html();
                        var div_pageBreaker = '<div style="page-break-before:always;"></div>';
                        var per_page = 25;
                        $('#table_data').each(function(index, element)
                        {
                            //how many pages of rows have we got?
                            var pages = Math.ceil($('tbody tr').length / per_page);

                            //if we only have one page no more
                            if (pages == 1) {
                                return;
                            }
                            //get the table we're splutting
                            var table_to_split = $(element);

                            var current_page   = 1;
                            //loop through each of our pages
                            for (current_page = 1; current_page <= pages; current_page++) 
                            {
                                //make a new copy of the table
                                var cloned_table = table_to_split.clone();
                                //remove rows on later pages
                                $('tbody tr', table_to_split).each(function(loop, row_element) {
                                    //if we've reached our max
                                    if (loop >= per_page) {
                                        //get rid of the row
                                        $(row_element).remove();
                                    }
                                });

                                //loop through the other copy
                                $('tbody tr', cloned_table).each(function(loop, row_element) {
                                    //if we are before our current page
                                    if (loop < per_page) {
                                        //remove that one
                                        $(row_element).remove();
                                    }
                                });

                                //insert the other table afdter the copy
                                if (current_page < pages) {
                                    $(div_pageBreaker).appendTo('#lastDataTable');
                                    $(printHeader).appendTo('#lastDataTable');
                                    $(cloned_table).appendTo('#lastDataTable');
                                }

                                //make a break
                                table_to_split = cloned_table;
                            }
                        });
                    });
                </script>
              </body>
            </html>
thefredzx
  • 41
  • 2
  • awesome...I'll check it out when I get time and report back. Though I'd already moved on from this issue – Stephen M Mar 02 '14 at 21:17
  • Looks good, but in my case I'm using rowspans (header cell spans trough multiple tr's. I'd suggest using a custom class to flag where to cut the table. – piotr_cz Jan 14 '16 at 11:14
2

This is an enhancement still not available in Webkit, Blink, and Vivliostyle, rather than in other more "print oriented formatters" (Firefox, IE).

You can check the issue for Google Chrome since version 4 here (6 years and 45 versions ago!), where we can appreciate it's got an owner lately (February 2016), who even seems to be working on it.

Some talk has been also held in the W3 where we can appreciate the concern upon it's usefulness:

Since repeating table headers and footers on a fragmentation break is generally a useful thing, I suggest we make a normative requirement and say that UAs must repeat header/footer rows when a table spans a break.

Meanwhile, the JS and Jquery codes from @DoctorDestructo and @thefredzx have been really useful for my users not using Firefox nor IE (most of them).

The first one to be aware of a new version that includes this feature, should notice it in here, many of us would appreciate it.

DavidTaubmann
  • 3,223
  • 2
  • 34
  • 43
  • Looks like your Chrome bug link is pointing to the W3 discussion (interesting read, btw). Bug is [here](https://bugs.chromium.org/p/chromium/issues/detail?id=24826&q=print%20thead&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified). – DoctorDestructo Apr 15 '16 at 01:31
1

From my testing in chrome setting display: table-row-group; to the thead stops the issue happening.

eg if you try to print the below without the style you will see number on top of each page but if you add the style it only appears oncontextmenu.

<style>
  thead {
      display: table-row-group;
  }
</style>
<table>
  <thead>
    <tr>
      <th>number</th>
    </tr>
  </thead>
  <tbody id="myTbody">
  </tbody>
</table>
<script>
  for (i = 1; i <= 100; i++) {
    document.getElementById("myTbody").innerHTML += "<tr><td>" + i + "</td></tr>";
  }
</script>
ak85
  • 4,154
  • 18
  • 68
  • 113
0

Remark from 2019: I was struggling for hours, had an image in my header

<table>
 <thead>
    <tr>
      <td>
        <img ...>
      </td>

which was only showing on first page.

Cause: Chrome adds a style @media print { img { page-break-inside: avoid !important; } }

This was causing the image not to show on page 2 and on!

Solution: So just insert a rule

@media print { thead img { page-break-inside: auto !important; } }

to deactivate this browser specific rule.

olidem
  • 1,961
  • 2
  • 20
  • 45
-4

for in-company browser based systems, i advice users to use firefox or IE instead;for websites intended for the public, i think we can't really do anything about it if users user chrome or browsers with similar limitations (opera also)

Manny
  • 11
  • 1