Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 23 – Debugging, performance, accessibility, tooling and print

Print stylesheets and paged media

Write print CSS that drops page chrome, prints link addresses and keeps tables whole, then control A4 pages, margins, breaks and printed colours.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Write @media print rules that remove page chrome, show link addresses and keep content readable
  • Set page size, margins and breaks with @page and the break properties
  • Use print-color-adjust to print the colour backgrounds that carry meaning
  • Predict where a long table breaks across pages, with its header repeated

Before you start

What you’ll learn · Before you start

What you will learn

  • Write @media print rules that remove page chrome, show link addresses and keep content readable
  • Set page size, margins and breaks with @page and the break properties
  • Use print-color-adjust to print the colour backgrounds that carry meaning
  • Predict where a long table breaks across pages, with its header repeated

Before you start

On this page

A print stylesheet decides what a page looks like on paper, or in a PDF saved from the print dialog. It removes what only makes sense on a screen, such as the navigation, buttons, sidebars and cookie banners; sets readable black text on white; prints the address after each external link, because nobody can click paper; and keeps figures and tables from being cut in half. Paged media CSS then controls the pages themselves: @page sets the paper size and the margins, the break- properties say where a page may or must end, and print-color-adjust decides whether colours that carry meaning, such as a status badge, survive printing. Put the rules in a stylesheet linked with media="print", or in an @media print block, and nothing on screen changes.

This lesson prints an article and an invoice to A4 PDFs with Chromium 153 and counts what changed, page by page, and checks the same print styles in Firefox 155.

Adding print rules, and seeing them without paper

There are two ways in, and both are standard. A separate file, <link rel="stylesheet" href="print.css" media="print">, applies only when printing, and because its media does not match a screen it never holds up the first paint, although the browser still downloads it at a low priority. An @media print { … } block inside an existing stylesheet keeps print rules next to the screen rules they adjust. Either way, place the print rules after the screen rules: with equal specificity, the later declaration wins where both apply.

You do not need a printer to see the result. Chrome’s Rendering tab can emulate the print media type, Firefox’s Rules view has a print toggle and Safari’s Elements tab a Force print media styles button, all covered in the DevTools lesson. Emulation shows the print styles on screen but not the page breaks; for those, open the print preview, or save a PDF.

A print stylesheet for an article

This guide page has a site header with navigation, a sidebar, a comments box, a sticky share bar, a footer and a fixed cookie banner. Its print stylesheet is linked with media="print":

An article page, its screen styles and its print stylesheet

article/article.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Choosing colours for a dashboard</title>
  <link rel="stylesheet" href="screen.css">
  <!-- Applies only when printing, and never holds up the first paint on screen. -->
  <link rel="stylesheet" href="print.css" media="print">
</head>
<body>
  <header class="site"><a class="logo" href="/">Field Notes</a>
    <nav><a href="/guides/">Guides</a> <a href="/tools/">Tools</a> <a href="/about/">About</a> <button type="button">Subscribe</button></nav>
  </header>
  <div class="layout">
    <main>
      <article>
        <h1>Choosing colours for a dashboard</h1>
        <p class="lede">A dashboard is read in a hurry, often on a bad screen, by people who have to act on what they see. Colour can speed that up or get in the way.</p>
        <h2>Start from the data, not the brand</h2>
        <p>Before picking a single colour, sort what the dashboard shows into three kinds: values that are ordered, such as load from low to high; values that are categories, such as regions; and values that need attention, such as a failing check. Each kind wants a different palette, and mixing them up is the most common reason a chart is hard to read.</p>
        <p>Ordered values need a sequential palette that changes mostly in lightness, so the order survives in greyscale and for readers who do not see some hues. Categories need colours that differ clearly from each other but have similar lightness, so no category looks more important than another. Alerts need one colour that is used for nothing else.</p>
        <h2>Check contrast where the colours meet</h2>
        <p>Text on a coloured tile needs the same contrast as text anywhere else. Lines and bars that carry meaning need 3:1 against the background, and so do the borders of controls. The <a href="https://www.w3.org/TR/WCAG22/">Web Content Accessibility Guidelines</a> state both thresholds, and a contrast checker tells you within seconds whether a pair passes.</p>
        <figure>
          <svg viewBox="0 0 320 120" width="320" height="120" role="img" aria-label="Five bars from light to dark blue"><rect x="10" y="70" width="50" height="40" fill="#dbeafe"/><rect x="70" y="55" width="50" height="55" fill="#93c5fd"/><rect x="130" y="40" width="50" height="70" fill="#3b82f6"/><rect x="190" y="25" width="50" height="85" fill="#1d4ed8"/><rect x="250" y="10" width="50" height="100" fill="#1e3a8a"/></svg>
          <figcaption>A sequential palette: lightness carries the order.</figcaption>
        </figure>
        <h2>Never use colour alone</h2>
        <p>Pair every colour that carries meaning with a second cue: a label, an icon, a pattern or a position. A red dot means nothing to a reader who cannot tell red from green, and nothing at all on a black-and-white printout of the weekly report.</p>
        <p>The same goes for states. A selected filter can be bold as well as blue; a failing check can say "Failed" as well as turn red. The second cue costs a few pixels and saves a support call.</p>
        <h2>Test the palette in the conditions people use</h2>
        <p>Look at the dashboard on a cheap laptop screen at an angle, on a phone in daylight and printed in greyscale. Run it through a colour blindness simulation and through forced colours mode. Anything that only works on a calibrated monitor in a dark room will fail someone.</p>
        <table>
          <caption>Palette checks before release</caption>
          <thead><tr><th>Check</th><th>Tool</th><th>Pass when</th></tr></thead>
          <tbody>
            <tr><td>Text contrast</td><td>Contrast checker</td><td>4.5:1, or 3:1 for large text</td></tr>
            <tr><td>Graphics contrast</td><td>Contrast checker</td><td>3:1 against the background</td></tr>
            <tr><td>Colour vision</td><td>Simulator</td><td>Every category still distinct</td></tr>
            <tr><td>Greyscale print</td><td>Printer</td><td>Order and alerts still readable</td></tr>
          </tbody>
        </table>
        <h2>Write the rules down</h2>
        <p>Put the palettes, their purposes and their contrast results in the design system, next to the components that use them. A palette without a written purpose drifts within months, as each new chart borrows whichever colour looks nice. More on that in the <a href="https://example.com/guides/design-tokens/">guide to design tokens</a>.</p>
      </article>
      <section class="comments"><h2>Comments</h2><p>Sign in to join the discussion.</p></section>
    </main>
    <aside class="sidebar"><h2>Related guides</h2><ul><li><a href="/guides/charts/">Picking a chart type</a></li><li><a href="/guides/tables/">Tables that scan</a></li><li><a href="/guides/legends/">Legends people read</a></li></ul></aside>
  </div>
  <div class="share">Share: <button type="button">Copy link</button> <button type="button">Email</button></div>
  <footer class="site-footer"><p>Field Notes · Guides for people who build interfaces</p><p><a href="/privacy/">Privacy</a> · <a href="/contact/">Contact</a></p></footer>
  <div class="cookie">This site uses one cookie to remember your theme. <button type="button">OK</button></div>
</body>
</html>

article/screen.css

/* The page's normal styles: they apply on screen and, unless print.css overrides them, in print too. */
body { margin: 0; font: 17px/1.6 system-ui, sans-serif; color: #1f2937; background: #f8fafc; }
.site { display: flex; justify-content: space-between; align-items: center; padding: 12px 24px; background: #0f172a; color: #fff; }
.site a { color: #fff; }
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 32px; max-inline-size: 1100px; margin: 24px auto; padding-inline: 24px; }
.sidebar { padding: 16px; background: #e2e8f0; border-radius: 8px; }
.lede { font-size: 1.2em; color: #334155; }
figure { margin: 24px 0; }
table { border-collapse: collapse; inline-size: 100%; }
th, td { padding: 8px; border-block-end: 1px solid #cbd5e1; text-align: start; }
.comments { margin-block-start: 48px; padding: 16px; background: #fff; }
.share { position: sticky; inset-block-end: 0; padding: 12px 24px; background: #fff; border-block-start: 1px solid #cbd5e1; }
.site-footer { padding: 24px; background: #0f172a; color: #cbd5e1; }
.site-footer a { color: #fff; }
.cookie { position: fixed; inset: auto 16px 16px; padding: 16px; background: #1e293b; color: #fff; border-radius: 8px; }

article/print.css

/* The print stylesheet. It is linked with media="print", so none of it applies on screen. */
@page {
  size: A4;
  margin: 18mm 16mm;
}

/* Page chrome that means nothing on paper. */
.site nav, .sidebar, .comments, .share, .site-footer, .cookie { display: none; }

/* One column, black on white, a size that reads well on paper. */
body { font: 11pt/1.5 Georgia, "Times New Roman", serif; color: #000; background: none; }
.site { padding: 0 0 6mm; background: none; color: #000; }
.site a { color: #000; text-decoration: none; }
.layout { display: block; max-inline-size: none; margin: 0; padding: 0; }

/* Links: keep them recognisable, and print the address of every external one after its text. */
a { color: inherit; }
article a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; overflow-wrap: anywhere; }

/* Keep figures and tables whole, and headings with what follows them where the browser can. */
figure, table { break-inside: avoid; }
h2 { break-after: avoid; }
p { orphans: 3; widows: 3; }

Printed to A4 by Chromium, with 10 mm margins when the page set none:

Without print.css With print.css
Pages 3 2
Navigation, sidebar, comments, footer printed gone
The fixed cookie banner printed on every page gone
The table and its caption caption at the foot of page 2, table on page 3 together on page 2
The link to the WCAG 2.2 standard printed as plain link text followed by its address in brackets
Text colour the screen’s greys and link blue black

Two of those results are typical of printing without a print stylesheet. A position: fixed element belongs to the viewport, and on paper each page is a viewport, so the cookie banner appeared at the bottom of all three pages. And the table’s caption stayed behind when the table moved on, because nothing asked the browser to keep them together; break-inside: avoid on the table did. The link addresses come from one rule: a[href^="http"]::after { content: " (" attr(href) ")"; } prints each external address after the link text, and overflow-wrap: anywhere stops a long address from running off the page. Firefox applied the same rules under its print emulation: the sidebar was hidden, the body font was Georgia, and the generated address was there.

Pages: size and margins with @page

@page sets up the page box. size: A4 (or A5, letter, A4 landscape, or two lengths) chooses the paper, and margin the space around the content, best given in millimetres or inches because paper is measured that way. A CSS inch is 96 px, so the content area of an A4 page with 14 mm margins is 269 mm, about 1,016 CSS px tall.

Feature What it does Support
@page { size; margin } Paper size and margins Baseline newly available: Safari added size last
@page :first Different margins for the first page Baseline widely available
@page :left, :right Different margins for facing pages Chrome and Safari, not Firefox
page: wide and @page wide { … } Named pages, for example a landscape page for a wide table The page property is widely available, @page itself newly available
Margin boxes such as @top-center Running headers, page numbers Chromium only

The print dialog still has the last word: people can choose another paper size, scale the page or change the margins, so a print stylesheet should survive A4 and US letter alike.

Breaks: where a page may end

The break properties say what may happen between and inside boxes. break-before: page and break-after: page force a new page, for example before each chapter. break-inside: avoid asks the browser not to split a box, such as a figure, a table row or a totals block, if it can avoid it. Their support differs by value:

Declaration Use it for Support
break-before: page, break-after: page Start each section on a new page Baseline widely available
break-inside: avoid Figures, table rows, cards, a totals block Baseline widely available
break-after: avoid Keep a heading with the paragraph after it Chromium only
orphans: 3, widows: 3 At least three lines of a paragraph at the foot and the top of a page Chrome and Safari, not Firefox

The older page-break-before, page-break-after and page-break-inside are kept as legacy aliases of the break- properties: they still work, but new code uses the new names. For tables, a <thead> is displayed as a table-header-group, and CSS 2 allows printing user agents to repeat such header rows on every page a table spans; Chromium did that for the invoice below.

A 40-row invoice on two A4 pages: page 1 has the brand band, addresses and rows 1 to 21; page 2 repeats the headings, rows 22 to 40, totals.Page 1 · A4, 14 mm marginsPage 2Brand band and Paid badgeprinted in colour: print-color-adjust: exactFrom and Bill toColumn headingsRows 1 to 21whole rows only: break-inside: avoidColumn headings againthead repeats: table-header-groupRows 22 to 40Subtotal, tax and totalkept in one piecethe next row did not fit

How the 40-row invoice broke across two printed pages

Text description of the diagram

The diagram shows two pages, one under the other.

Page 1, A4 with 14 mm margins, holds from top to bottom: the brand band with the Paid badge, printed in colour because of print-color-adjust: exact; the From and Bill to addresses; the column headings; and rows 1 to 21, only whole rows because of break-inside: avoid.

An arrow labelled "the next row did not fit" leads to page 2, which holds: the column headings again, because the table's thead is a table-header-group that repeats; rows 22 to 40; and the subtotal, tax and total, kept together in one piece.

Colours that must print

Browsers save ink by default. Unless the person printing ticks an option such as “Background graphics”, background colours and images are left out, and the CSS Color Adjustment specification lets the browser adjust text colours too. That is the default value, print-color-adjust: economy. print-color-adjust: exact marks an element whose colours carry meaning and asks the browser to print them as they are. It is Baseline newly available.

The invoice below has a teal brand band with white text and a green “Paid” badge. Printed with backgrounds off, which is how the dialog usually starts:

Print rules Brand band and badge Their white text
no print stylesheet not painted printed light grey (#ababab) on white
print stylesheet without print-color-adjust not painted light grey on white
print stylesheet with print-color-adjust: exact teal and green white, readable

Without exact, the status that the badge exists to show is gone from the page. Use exact only for such elements, never for the whole page: a large printed background costs the reader ink and does not help them read.

A worked example: an invoice on one A4 page

The invoice sits inside a billing app: a toolbar with Send, Download and Print buttons, a navigation column and a help footer. Its print rules are in a separate stylesheet, inside @media print:

An invoice, the app's screen styles and the invoice's print rules

invoice/invoice.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Invoice NL-2417</title>
  <link rel="stylesheet" href="invoice.css">
  <link rel="stylesheet" href="invoice-print.css">
</head>
<body>
  <header class="toolbar">Billing <span class="actions"><button type="button">Send</button> <button type="button">Download</button> <button type="button">Print</button></span></header>
  <nav class="side"><a href="#invoices">Invoices</a> <a href="#customers">Customers</a> <a href="#settings">Settings</a></nav>
  <main class="sheet">
    <div class="brand"><strong>Nilgiri Leaf Company</strong> <span class="status">Paid</span></div>
    <h1>Tax invoice NL-2417</h1>
    <div class="parties">
      <p><b>From</b><br>Nilgiri Leaf Company<br>14 Hill Road, Coonoor 643101</p>
      <p><b>Bill to</b><br>Meera Iyer<br>22 Lake View, Bengaluru 560034</p>
    </div>
    <table class="items">
      <thead><tr><th>Description</th><th class="num">Qty</th><th class="num">Rate</th><th class="num">Amount</th></tr></thead>
      <tbody>
          <tr><td>Nilgiri green tea, 250 g</td><td class="num">4</td><td class="num">₹450.00</td><td class="num">₹1,800.00</td></tr>
          <tr><td>Assam breakfast tea, 500 g</td><td class="num">2</td><td class="num">₹620.00</td><td class="num">₹1,240.00</td></tr>
          <tr><td>Darjeeling first flush, 100 g</td><td class="num">3</td><td class="num">₹780.00</td><td class="num">₹2,340.00</td></tr>
          <tr><td>Masala chai blend, 250 g</td><td class="num">6</td><td class="num">₹340.00</td><td class="num">₹2,040.00</td></tr>
          <tr><td>Lemongrass infusion, 100 g</td><td class="num">5</td><td class="num">₹210.00</td><td class="num">₹1,050.00</td></tr>
          <tr><td>Tulsi tea bags, 25 bags</td><td class="num">8</td><td class="num">₹160.00</td><td class="num">₹1,280.00</td></tr>
          <tr><td>Kashmiri kahwa, 100 g</td><td class="num">2</td><td class="num">₹540.00</td><td class="num">₹1,080.00</td></tr>
          <tr><td>Glass tea pot, 600 ml</td><td class="num">1</td><td class="num">₹1,450.00</td><td class="num">₹1,450.00</td></tr>
          <tr><td>Steel strainer</td><td class="num">4</td><td class="num">₹120.00</td><td class="num">₹480.00</td></tr>
          <tr><td>Clay cups, set of 6</td><td class="num">2</td><td class="num">₹480.00</td><td class="num">₹960.00</td></tr>
          <tr><td>Gift box, large</td><td class="num">3</td><td class="num">₹260.00</td><td class="num">₹780.00</td></tr>
          <tr><td>Shipping</td><td class="num">1</td><td class="num">₹150.00</td><td class="num">₹150.00</td></tr>
      </tbody>
    </table>
    <div class="totals">
      <p>Subtotal <span>₹14,650.00</span></p>
      <p>Tax (5 %) <span>₹732.50</span></p>
      <p class="grand">Total <span>₹15,382.50</span></p>
    </div>
    <p class="note">This is a made-up invoice for a lesson on print layout. It shows layout, not tax rules.</p>
  </main>
  <footer class="app-footer">Need help? Open the help centre.</footer>
</body>
</html>

invoice/invoice.css

/* The billing app's screen styles. */
body { display: grid; grid-template-columns: 180px 1fr; grid-template-rows: auto 1fr auto; margin: 0; min-block-size: 100vh; font: 15px/1.5 system-ui, sans-serif; background: #f1f5f9; color: #0f172a; }
.toolbar { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 12px 20px; background: #0f172a; color: #fff; }
.side { display: flex; flex-direction: column; gap: 8px; padding: 20px; background: #e2e8f0; }
.sheet { max-inline-size: 760px; margin: 24px; padding: 32px; background: #fff; box-shadow: 0 2px 12px rgb(15 23 42 / 12%); }
.brand { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: #0f766e; color: #fff; }
.status { padding: 2px 10px; border-radius: 999px; background: #16a34a; color: #fff; font-weight: 700; }
.parties { display: flex; justify-content: space-between; gap: 24px; }
.items { inline-size: 100%; border-collapse: collapse; }
.items th, .items td { padding: 6px 8px; border-block-end: 1px solid #cbd5e1; text-align: start; }
.items th { background: #f1f5f9; }
.num { text-align: end; font-variant-numeric: tabular-nums; }
.totals { margin-inline-start: auto; inline-size: 260px; }
.totals p { display: flex; justify-content: space-between; margin: 4px 0; }
.grand { font-weight: 700; border-block-start: 2px solid #0f172a; padding-block-start: 4px; }
.note { color: #475569; font-size: 13px; }
.app-footer { grid-column: 1 / -1; padding: 12px 20px; background: #e2e8f0; }

invoice/invoice-print.css

/* What changes on paper. All of it sits in @media print, so the screen is unaffected. */
@media print {
  @page { size: A4; margin: 14mm; }

  /* The app around the invoice: not part of the document. */
  .toolbar, .side, .app-footer { display: none; }
  body { display: block; min-block-size: 0; background: none; }
  .sheet { max-inline-size: none; margin: 0; padding: 0; box-shadow: none; }

  /* The brand band and the status badge carry meaning: print their colours even when the browser drops backgrounds. */
  .brand, .status { print-color-adjust: exact; }

  /* Long invoices: the column headings repeat on every page, rows never split, totals stay in one piece. */
  .items thead { display: table-header-group; }
  .items tr, .totals { break-inside: avoid; }
}

India-specific

The sample is a made-up tax invoice in rupees, with amounts in the Indian digit grouping (₹15,382.50) and the “From” and “Bill to” blocks of a GST-style layout. It demonstrates print layout only; it is not a guide to what a GST invoice must contain.

Without invoice-print.css With it
Pages for 12 items 2: the invoice, then a page holding only the app’s help line 1
Toolbar, navigation, help footer printed gone
Brand band and Paid badge, backgrounds off not painted printed in colour
The same invoice with 40 items — 2 pages: rows 1 to 21, then the headings again with rows 22 to 40 and the totals

The second page of the 40-row invoice began with the column headings, repeated by the browser, and the subtotal, tax and total stayed together after the last row. In Firefox, under print emulation, the same rules applied: the toolbar was hidden, the badge had print-color-adjust: exact, the <thead> was a table-header-group and the rows had break-inside: avoid.

HTML to PDF Converter Turn an HTML page, with its print styles, into a PDF without a print dialog.

Practise: break a long table into pages

Exercise · Medium · JavaScript

Work out where a long invoice table breaks across pages

When a long table is printed, the browser fills each page with whole rows, repeats the table's header at the top of every page and moves anything that must not be split to the next page. Write that calculation in pages.mjs, as two exported functions.

1. contentHeight(paper, margins) returns the height of a page's content area in CSS pixels, rounded down to a whole pixel. paper is 'A4' (297 mm tall), 'A5' (210 mm) or 'letter' (279.4 mm), and margins is { top, bottom } in millimetres. A CSS inch is 96 px and 25.4 mm. 2. paginate({ pageHeight, headerHeight, rows, firstPageOffset, totalsHeight }) returns the pages as a list, each page a list of the indexes of its rows. All heights are in px; firstPageOffset and totalsHeight default to 0. - Every page that has rows starts with the table's header, headerHeight tall. The first page also holds firstPageOffset px of other content above the table. - Rows are placed in order and never split. A row that does not fit in what is left of the page starts a new page, unless the page has no rows yet: a row taller than a whole page then gets that page to itself. - The totals block, totalsHeight tall, comes after the last row and stays on the same page. When it does not fit after the last row, the last row moves to a new page with it, unless it is the only row on its page. - A table without rows has no pages: return [].

The sample tests import both functions from pages.mjs and run in your browser.

Starter code · pages.mjs

/** The height of a page's content area in CSS px: 96 px to the inch, 25.4 mm to the inch, rounded down. */
export function contentHeight(paper, margins) {
  // Replace this line with your code.
  return 0;
}

/** Which rows of a long table go on which page: a list of pages, each a list of row indexes. */
export function paginate({ pageHeight, headerHeight, rows, firstPageOffset = 0, totalsHeight = 0 }) {
  // Replace this line with your code.
  return [];
}
The sample tests · pages.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { contentHeight, paginate } from './pages.mjs';

const range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
const rows = (count, height) => Array(count).fill(height);

test('content height of a page, from paper and margins', () => {
  assert.equal(contentHeight('A4', { top: 14, bottom: 14 }), 1016);
  assert.equal(contentHeight('A5', { top: 10, bottom: 10 }), 718);
  assert.equal(contentHeight('letter', { top: 12.7, bottom: 12.7 }), 960);
});

test('the header repeats, and the first page has less room', () => {
  const pages = paginate({ pageHeight: 1000, headerHeight: 40, rows: rows(30, 36), firstPageOffset: 300 });
  assert.deepEqual(pages, [range(0, 17), range(18, 29)]);
});

test('rows of different heights never split', () => {
  const pages = paginate({ pageHeight: 400, headerHeight: 40, rows: [100, 120, 150, 90, 200] });
  assert.deepEqual(pages, [[0, 1], [2, 3], [4]]);
});

test('the totals stay with the last row', () => {
  assert.deepEqual(paginate({ pageHeight: 1000, headerHeight: 40, rows: rows(30, 36), firstPageOffset: 300, totalsHeight: 120 }), [range(0, 17), range(18, 29)]);
  assert.deepEqual(paginate({ pageHeight: 1000, headerHeight: 40, rows: rows(44, 36), firstPageOffset: 300, totalsHeight: 120 }), [range(0, 17), range(18, 42), [43]]);
});

test('a row taller than a page gets a page of its own', () => {
  assert.deepEqual(paginate({ pageHeight: 300, headerHeight: 40, rows: [100, 400, 100] }), [[0], [1], [2]]);
});

test('edge cases', () => {
  assert.deepEqual(paginate({ pageHeight: 300, headerHeight: 40, rows: [] }), []);
  assert.deepEqual(paginate({ pageHeight: 300, headerHeight: 40, rows: [200], totalsHeight: 100 }), [[0]]);
  assert.deepEqual(paginate({ pageHeight: 300, headerHeight: 40, rows: [130, 130] }), [[0, 1]]);
});
A hint

Keep a running total of the height used on the current page, starting at firstPageOffset + headerHeight. Before placing a row, check whether it still fits; when it does not and the page already has rows, close the page and start a new one at headerHeight. Deal with the totals once, after the last row has been placed.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A page has a cookie banner with position fixed and no print stylesheet. What happens when it is printed on three pages?

    Choose one answer.

    Show the answer to question 1

    Answer: The banner is printed on every page

    On paper each page acts as the viewport, so a fixed box is repeated on every page. In the lesson's run the banner sat at the bottom of all three pages until the print stylesheet hid it with display none.

  2. Question 3 of 6 An A4 page (297 mm tall) has @page { margin: 14mm; }. How tall is its content area in CSS pixels, rounded down? (1 in = 96 px = 25.4 mm)

    Type a number.

    Show the answer to question 3

    Answer: 1016 px (anything from 1015 to 1017 counts)

    297 − 2 × 14 = 269 mm, and 269 × 96 ÷ 25.4 = 1,016.7, so 1,016 whole pixels. The exercise works with this number to decide how many table rows fit on a page.

  3. Question 4 of 6 Which of these work in Chromium, Firefox and WebKit alike?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • break-before: page on each chapter heading
    • break-inside: avoid on a table row

    break-inside avoid and break-before page are Baseline widely available. break-after avoid works only in Chromium, and widows and orphans are not supported in Firefox, so treat those two as hints.

  4. Question 5 of 6 A green "Paid" badge with white text prints as light grey text on white paper. What fixes it?

    Choose one answer.

    Show the answer to question 5

    Answer: print-color-adjust: exact on the badge

    Browsers drop backgrounds when printing unless the reader asks for them, and may adjust text colours: the lesson's run printed the white text as #ababab with no green. print-color-adjust exact marks colours that carry meaning, and the badge then printed green with white text.

  5. Question 6 of 6 A 40-row invoice table spans two pages. What makes its column headings appear at the top of the second page as well?

    Choose one answer.

    Show the answer to question 6

    Answer: The headings are in a thead, which is displayed as a table-header-group

    CSS 2 lets printing user agents repeat table-header-group rows on every page a table spans, and Chromium did: the second page of the lesson's invoice began with the headings, then rows 22 to 40 and the totals.

Interview questions

Fresher · What does a print stylesheet usually change, and how can you check it without printing?

It removes what only works on a screen: the navigation, buttons, sidebars, sticky bars and cookie banners, usually with display: none. It sets dark text on a white background in a size that reads well on paper, prints the address after external links with a[href^="http"]::after { content: " (" attr(href) ")"; }, and keeps figures, table rows and similar boxes whole with break-inside: avoid. @page sets the paper size and margins. To check it, emulate the print media type in DevTools (Chrome’s Rendering tab, Firefox’s print toggle in the Rules view, Safari’s Force print media styles button) to see the styles, then use the browser’s print preview or save a PDF to see where the pages break. Emulation alone does not show page breaks.

Key takeaways

  • Link print rules with media="print" or wrap them in @media print; neither changes the screen.
  • Hide the page chrome, and fixed elements in particular: a fixed cookie banner printed on every page.
  • Print external link addresses with attr(href) in ::after, and let long addresses wrap.
  • @page { size: A4; margin: 14mm; } sets the paper and margins; :first and named pages are widely available, :left, :right and margin boxes are not.
  • break-inside: avoid keeps figures, rows and totals whole; break-before: page starts a new page; thead repeats on every page a table spans.
  • Backgrounds are dropped by default; print-color-adjust: exact keeps the colours that carry meaning.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.