How to write a print stylesheet that actually works

Updated 2026-08-26 · 10 min read

Short answer: Wrap print rules in @media print, hide navigation and widgets, reset the article container to a single block-level column, set margins with @page, and control pagination with break-inside: avoid on figures and break-after: avoid on headings. Ship the legacy page-break-* aliases alongside the modern properties, because engines still disagree.

Print CSS is the last unloved stylesheet on most sites, and unusually cheap to get right: about forty lines takes you from "the nav bar owns sheet one" to something a reader would keep. What follows is a stylesheet you can paste and trim, plus what will break it.

Start from the preview, not from the CSS

Open your own article and press Ctrl+P before you write a single rule. That preview is the specification. Note every element that should not be there, every place the text is clipped, and every page that is blank. You will usually find the same six problems on every template, and you will fix them once.

Note: In Chrome DevTools you can press Ctrl+Shift+P, run Show Rendering, and set Emulate CSS media type to print. That is fast for iterating on colours and hidden elements, but it does not paginate: page breaks, @page margins and orphan control only appear in the real print preview. Use both.

Layer 1: hide what paper does not need

@media print {
  /* Interface that means nothing on paper */
  nav, aside, footer, form, video, iframe, dialog,
  .site-header, .sidebar, .comments, .newsletter,
  .share-buttons, .cookie-banner, .related-posts,
  [role="banner"], [role="navigation"], [role="complementary"],
  [data-print="hide"] {
    display: none !important;
  }

  /* Give the article the whole sheet */
  html, body {
    margin: 0;
    padding: 0;
    width: auto;
    background: #fff;
    color: #000;
    font: 11pt/1.45 Georgia, "Times New Roman", serif;
  }

  main, article, .content, .entry-content {
    display: block;      /* flex and grid fragment badly across pages */
    float: none;
    width: auto;
    max-width: none;
    margin: 0;
    padding: 0;
    overflow: visible;   /* a clipped wrapper prints one page and stops */
    height: auto;
  }
}

Three of those lines do most of the work. display: block on the article container matters because flex and grid containers fragment unpredictably across pages, and a two-column card grid will happily slice a card in half at the page boundary. overflow: visible and height: auto matter because a wrapper with height: 100vh or overflow: hidden truncates your document to exactly one page, which is the single most common cause of "only the first page prints".

Layer 2: the page box and the breaks

@page {
  margin: 18mm 16mm 20mm 16mm;   /* top right bottom left */
}

@media print {
  /* Never split these */
  figure, table, pre, blockquote, img, li, .card {
    break-inside: avoid;
    page-break-inside: avoid;      /* legacy alias, still honoured by some engines */
  }

  /* Never strand a heading at the foot of a page */
  h1, h2, h3, h4 {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* At least three lines of a paragraph on each side of a break */
  p, li { orphans: 3; widows: 3; }

  /* Repeat table headers on every page */
  thead { display: table-header-group; }
  tfoot { display: table-footer-group; }

  /* Optional: a wide table gets its own landscape sheet (Chromium 110+) */
  @page wide { size: A4 landscape; }
  .table-wide { page: wide; }
}

Watch out: Do not put size: A4 on the bare @page rule of a site with an international audience. A reader on US Letter then gets an A4 page scaled or clipped onto Letter paper. Setting only the margin is portable; the paper size belongs to the person holding the printer.

What you wantModern propertyShip alongside itReality check
Keep a figure wholebreak-inside: avoidpage-break-inside: avoidSolid in Chromium and Safari. Firefox is patchy on flex and grid children
Start a section on a fresh sheetbreak-before: pagepage-break-before: alwaysWorks everywhere. Never apply it to the first heading or you get a blank opening sheet
Keep a heading with its textbreak-after: avoidpage-break-after: avoidChromium honours it. Firefox often ignores it on headings
No stranded single linesorphans: 3; widows: 3nothingChromium and Safari only. Firefox has never implemented it
Set the paper margins@page { margin: 18mm }nothingWidely supported, but the user can still override it in the print dialog
Repeat table headersthead { display: table-header-group }nothingReliable, and one of the highest-value rules on this list
Force background coloursprint-color-adjust: exact-webkit-print-color-adjust: exactA request, not a guarantee. Never let a background colour carry meaning

What is safe to rely on, as of 2026.

On paper a hyperlink is decoration. Printing the destination after the link text turns a printout into a document a reader can act on, which matters most for documentation, references and anything academic.

@media print {
  a { color: #000; text-decoration: underline; }

  /* Print the destination after external links */
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 85%;
    word-break: break-all;   /* long URLs must not overflow the sheet */
  }

  /* ...but not for these */
  a[href^="#"]::after,
  a[href^="mailto:"]::after,
  a[href^="tel:"]::after,
  a[href^="javascript:"]::after,
  a.no-print-url::after,
  nav a::after {
    content: "";
  }

  /* Expand abbreviations too */
  abbr[title]::after { content: " (" attr(title) ")"; }
}

Tip: Add a.no-print-url to any link whose visible text is already the URL, or you will print https://example.com (https://example.com) on every reference line. The nav a::after reset is belt and braces for themes that keep a breadcrumb visible on paper.

Layer 4: colour, images and the dark-mode trap

The nastiest modern print bug is dark mode. If your colour tokens are redefined in a prefers-color-scheme: dark block and your print block does not redefine them, a reader whose operating system is set to dark gets a solid black A4 rectangle and an empty toner cartridge. Print styles must come after, and must reassert the colours.

@media print {
  /* Reassert light values regardless of the reader's colour scheme */
  :root {
    --bg: #fff;
    --fg: #000;
    --muted: #444;
    --line: #999;
  }

  body { background: #fff !important; color: #000 !important; }

  img, svg, canvas {
    max-width: 100% !important;
    height: auto !important;
    break-inside: avoid;
  }

  /* Keep code blocks readable and unbroken */
  pre, code {
    font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
    font-size: 9pt;
    white-space: pre-wrap;
    word-wrap: break-word;
    border: 1px solid #ccc;
  }

  /* A fixed header repeats or clips on every sheet */
  .site-header, .toolbar, [style*="position:fixed"] {
    position: static !important;
  }
}

Lazy-loaded images are the other silent failure. Chromium now eagerly loads loading="lazy" images before printing, but a custom IntersectionObserver loader will still print blank boxes for anything the reader never scrolled past. If you rolled your own, force the images in before the dialog opens:

// Load every deferred image before the print dialog paints
window.addEventListener('beforeprint', () => {
  document.querySelectorAll('img[data-src]').forEach((img) => {
    if (!img.src) img.src = img.dataset.src;
  });
  document.querySelectorAll('img[loading="lazy"]').forEach((img) => {
    img.loading = 'eager';
  });
});

// Same event, if you need to expand collapsed sections
window.addEventListener('beforeprint', () => {
  document.querySelectorAll('details').forEach((d) => (d.open = true));
});

The six bugs you will actually hit

SymptomCauseFix
Only the first page printsheight: 100vh or overflow: hidden on a wrapperSet both to auto and visible inside @media print
The whole sheet is solid blackDark-mode tokens applied and never overridden for printRedeclare the colour custom properties inside the print block
Images are missingLazy loading, or the image is a CSS backgroundForce-load on beforeprint; use <img> for content images, never background-image
A card grid slices a card in halfFlex and grid fragmentationSet the container to display: block for print
The header appears on every sheetposition: fixedSet position: static in @media print
A blank sheet at the endA trailing break-after: page, or a tall hidden footerAudit the last break rule; hide the footer entirely

A testing routine that catches the rest

  1. Test the real preview in three engines Chrome, Firefox and Safari paginate differently. Firefox in particular ignores orphans, widows and some break-after rules, so a layout that is perfect in Chrome can strand headings in Firefox.
  2. Test both paper sizes Switch the print dialog between A4 and Letter. A4 is 20mm narrower and 17mm taller than Letter, which is enough to push a wide table over the edge.
  3. Test in dark mode Set the operating system to dark, reload and open the preview. This catches the black-page bug that never shows up on a developer machine set to light.
  4. Test your longest and your widest page The longest article finds your break bugs; the widest data table finds your overflow bugs. Everything else sits between them.
  5. Save as PDF and read it Print to PDF and actually read the file at 100 percent. Check that the text is selectable, that the URLs are expanded, and that nothing is clipped at the margin.
  6. Check the headers you cannot control The browser adds its own title, URL, date and page numbers. You cannot remove them from CSS; the reader turns them off. Design assuming they are on.

Render a page to PDF to see exactly what your print stylesheet produces.

Extract the text from your printed PDF to confirm the text layer survived intact.

Now give readers a way to reach it: a print button on any site, or on WordPress the button routes. For the wider audit, run through making your website printer-friendly. To see how a page behaves with no print CSS at all, paste it into the web page printer.

Frequently asked questions

Why is my print CSS not working?

Three usual causes: the rules sit outside a @media print block, a dark-mode block later in the cascade overrides them, or you tested with the DevTools media emulator, which does not paginate. Always confirm in the real Ctrl+P preview.

What is the difference between break-inside and page-break-inside?

break-inside is the modern CSS Fragmentation property; page-break-inside is the legacy alias. Browsers map the old names onto the new ones, but engine support still differs in edge cases, so shipping both costs one line and removes the guesswork.

How do I print the URL of a link in CSS?

Use a[href^="http"]::after { content: " (" attr(href) ")"; }. Add resets with content: "" for anchors, mailto, tel and any link whose visible text is already the URL, and set word-break: break-all so long URLs do not overflow.

Can I control the paper size from CSS?

Yes, with @page { size: A4 } or size: letter, but you usually should not. A reader on the other paper size gets scaled or clipped output. Set only the margins and let the print dialog decide the sheet.

Can I remove the browser header and footer with CSS?

No. The page title, URL, date and page numbers are added by the browser, outside the document, and only the reader can turn them off in the print dialog. Design your margins on the assumption that they are switched on.

Tools

Keep reading