A web page on a screen and the same page on paper are different documents. The screen scrolls and has no edges. Paper has a fixed size, margins, and a place where a page ends. A heading can be left alone at the bottom of a page, a table can split one row from its header, and a coloured callout can print as plain white because browsers drop background colours by default. Print styles deal with all of this, and they are short.
Set the page
The @page rule sets the paper size and margins. Put it in your stylesheet, and it applies when the page is printed:
@page {
size: A4;
margin: 2cm;
}Use letter instead of A4 for North American users, or leave size out to let the browser use its default. Most browsers accept @page margins, but each has its own print dialog, and the user can override the paper size there. Design for the stated size and let the user change it.
Keep headings with the text under them
A heading at the bottom of a page, separated from its first paragraph, is the most visible print fault. Tell the browser not to break after headings, and not to split short blocks in the middle:
@media print {
h1, h2, h3, h4 {
break-after: avoid;
}
figure, blockquote, pre {
break-inside: avoid;
}
p {
orphans: 3; /* at least 3 lines at the bottom of a page */
widows: 3; /* and at least 3 lines at the top of the next */
}
}These are hints, not guarantees. A block taller than a page cannot avoid breaking, and the browser will break it. Support also varies a little between Chromium, Firefox and Safari, so check the output in each one rather than assuming the three match.
Tables that repeat their header
A long table printed across several pages should show its column headings on every page. Mark the header as a header group, and keep each row whole:
@media print {
thead {
display: table-header-group; /* repeats on each printed page */
}
tr {
break-inside: avoid; /* a row is never split across pages */
}
}The table-header-group value is the one that matters most. Without it, the first page has the headings and the rest of the table does not.
Keep the colours you designed
Browsers print background colours and images only when the user ticks an option, which most people leave off. A coloured callout that looks clear on screen can turn into bordered grey text on paper. Ask the browser to keep the colour for elements where it carries meaning:
@media print {
.callout,
.status-badge {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
}Only do this for elements where colour is information, such as a warning box or a status label. Printing every background is wasteful, and colour alone is a poor way to carry meaning on black-and-white paper. Keep a text label next to any coloured status.
Show the address of a link
A printed link is not clickable, so the reader cannot see where it goes. For external links, print the address after the text. Skip links that are only page anchors, because those mean nothing on paper:
@media print {
a[href^="http"]::after {
content: " (" attr(href) ")";
font-size: 90%;
}
.no-print {
display: none !important; /* toolbars, navigation, buttons */
}
}Printing one document, not the page
window.print() prints the whole page: the site header, the sidebar, the toolbar and everything else on screen. When the user wants a document, print that document alone. One way is to write the document into a hidden iframe with its own print styles and print the frame:
export function printDocument(html: string, css: string) {
// html must already be sanitised. The iframe shares this page's origin, so any
// script inside it would run with access to the site.
const frame = document.createElement("iframe");
frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0";
frame.srcdoc = `<!doctype html><html><head><meta charset="utf-8"><style>${css}</style></head>
<body>${html}</body></html>`;
frame.onload = () => {
frame.contentWindow?.focus();
frame.contentWindow?.print();
setTimeout(() => frame.remove(), 1000);
};
document.body.appendChild(frame);
}The comment in that example is the important part. The HTML being printed is the user’s own document, and an iframe created this way runs its scripts with the same origin as your site. The document should go through the same sanitiser you use when saving it, and that step belongs before the print call, not after. Otherwise printing a document becomes a way to run a stored script.
Test on paper, or print to PDF
Each engine paginates a little differently, so exact page breaks will not match across browsers, and they should not be promised to users. What you can test is that headings stay with their text, tables keep their headers, colours that carry meaning survive, and nothing from the interface appears on the page. Use the print preview in Chrome, Edge and Firefox, and print to PDF once in each. Pick one long document with a table, a figure, a code block and several headings, and look at every page.
Print styles are cheap to add and expensive to skip. A document that prints cleanly is the kind of detail a customer notices in a procurement review, even when nobody says so.
RichTextEditor is a perpetual-licence JavaScript editor — one purchase, self-hosted, no metered editor loads. Download the evaluation or see how it compares.