MonkeyLoader All articles
Performance

The Paper Copy Clause: Loading Print CSS Only When Someone Asks for It

MonkeyLoader
The Paper Copy Clause: Loading Print CSS Only When Someone Asks for It

Photo: John Robert McPherson, CC BY-SA 4.0, via Wikimedia Commons

Print styles have a funny reputation in front-end work. They are either an afterthought that gets bolted on at the end, or a 60KB collection of table rules, page-break tweaks and black-and-white logos that every visitor downloads because it was easier than splitting it out. On a council tax page, an invoice or an event ticket, the print version matters. On the homepage, it is dead weight with a paper tray.

The useful shift is to treat print as a separate delivery channel. Keep a small, always-present print skeleton in the main CSS so the page never prints like a car crash when JavaScript is unavailable. Then load the full print stylesheet only when someone shows intent: they open print preview, use a print button, or the browser reports that print media is active.

Print is a channel, not a checkbox

A lot of teams still rely on <link rel='stylesheet' href='print.css' media='print'>. That is better than nothing, but it is not a guarantee of laziness. Browsers treat print stylesheets differently. Some fetch them with low priority, some fetch them only when needed, and some fetch them anyway. The result is inconsistent: you cannot confidently say the big print file is out of the critical path, and you cannot confidently say it will be there the moment someone hits Ctrl+P.

The better pattern is a two-layer approach. Layer one is a compact print skeleton inside your main stylesheet: hide navigation, strip backgrounds, set sensible margins, prevent images from overflowing, and make links readable. That skeleton should be maybe a few hundred bytes to a couple of kilobytes. It is enough to produce a respectable page when JavaScript fails or when the browser prints before your enhanced stylesheet arrives.

Layer two is the full print layout, loaded on demand. That is where you put the expensive stuff: detailed invoice tables, repeated table headers, ticket cut lines, QR code placement, page-break rules for long council appendices, and the print-specific typography that makes a difference on paper but costs every screen visitor bandwidth.

The intent signals worth listening for

The most obvious signal is window.matchMedia('print'). When it matches, the browser is in print media. You can check it on load for the odd case where a page is opened directly in a print context, and listen for changes.

The second signal is beforeprint. Chrome, Edge and Firefox fire it before the print dialog, which gives you a chance to inject the full stylesheet. Safari and iOS Safari are less predictable, especially when printing through the share sheet. That is exactly why the skeleton matters. Do not bet the farm on beforeprint firing early enough to affect the preview.

A small loader can look like this:

const printCss = '/assets/print-full.css';
let printCssLoaded = false;
let printCssLoading = false;

function loadPrintCss(done) {
  if (printCssLoaded) {
    if (done) done();
    return;
  }

  if (printCssLoading) return;
  printCssLoading = true;

  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = printCss;
  link.media = 'print';
  link.onload = () => {
    printCssLoaded = true;
    printCssLoading = false;
    if (done) done();
  };
  link.onerror = () => {
    printCssLoading = false;
  };

  document.head.appendChild(link);
}

const printMedia = window.matchMedia('print');
if (printMedia.matches) loadPrintCss();

if (printMedia.addEventListener) {
  printMedia.addEventListener('change', event => {
    if (event.matches) loadPrintCss();
  });
}

window.addEventListener('beforeprint', () => loadPrintCss());

That is deliberately boring. No framework, no race condition theatre, no attempt to preload the file for everyone. If the full stylesheet fails, the skeleton still does a job.

Browser quirks are the whole job

The annoying part is not the loader. It is the timing.

In Chrome and Edge, a beforeprint listener usually gives you enough room to add the stylesheet before the preview is generated, but you should still test with the file cached and uncached. Firefox behaves reasonably well, but long documents can expose page-break differences. Safari is the one that keeps you honest. Printing from the share sheet on iOS may not give you the same event sequence as desktop Safari, and the preview can appear before your enhanced CSS has settled.

That means a print button should not be a lie. If you offer a button, load the stylesheet first, then call window.print() after the link has loaded. If the stylesheet is already loaded, print immediately. If it errors, print anyway with the skeleton. Never leave the user staring at a disabled button because a stylesheet request failed.

const printButton = document.getElementById('print-page');
if (printButton) {
  printButton.addEventListener('click', () => {
    loadPrintCss(() => window.print());
  });
}

For keyboard users, the browser print shortcut is still the main route. That is why beforeprint and matchMedia stay in the mix. The button is an extra, not the only door.

Accessible fallbacks, not accessibility theatre

A print stylesheet is not an accessibility feature on its own, but a broken print view can be an accessibility problem. Council pages often contain long tables, service contacts and downloadable forms. Invoices need clear totals, addresses and VAT information. Event tickets need the reference, date and venue to survive a monochrome printer.

Keep the skeleton semantic. Do not hide content that a paper reader needs just because it is interactive on screen. Expand abbreviations where you can. Make sure link URLs are not printed as giant unreadable strings unless they are genuinely useful. Avoid relying on colour to separate table rows. Check that focus styles and hidden form labels do not leak into the print output.

And do not forget the no-JavaScript case. A small inline or main-bundle print block is the accessible fallback. If someone on a locked-down machine hits print and your dynamic loader never runs, the page should still produce a sane document. That is the person who really does want a paper copy, and they should not be punished for it.

Testing on the pages that matter

Test the pages where print is a real task, not just the homepage. A council bin collection page, a council tax summary, an invoice with a VAT breakdown, and a village hall event ticket will expose different problems. Long tables test repeated headers. Invoices test page breaks and totals. Tickets test QR code sizing, cut lines and black-and-white legibility.

In Playwright, page.emulateMedia({ media: 'print' }) is a quick way to check that the correct styles apply. Pair that with a JavaScript-disabled run and a throttled connection test. Then do the old-fashioned thing: print to PDF in Chrome, Firefox, Safari and Edge, and look at the actual pages. Check the console for failed stylesheet requests. Check that the main bundle did not grow by another 40KB because someone pasted the whole print file back in during a Friday merge.

A useful rule for UK teams in 2026: if a page exists to be filed, posted or handed over at a desk, its print layout deserves a test. If it exists to be browsed, it does not need to carry the full paper wardrobe.

What to ship

Ship a small print skeleton, a separate full print stylesheet, and a loader that responds to matchMedia('print') and beforeprint. Add a print button that waits for the stylesheet before opening the dialog. Keep the no-JavaScript fallback intact. Test the council pages, invoices and tickets that people actually print, not the homepage nobody puts in a drawer.

All Articles

Related Articles

Dead Weight in the dist Folder: Hunting JavaScript Nobody Loads

Dead Weight in the dist Folder: Hunting JavaScript Nobody Loads

Switch It Off Before They Find It: Five Default Loaders That Fail a UK Accessibility Audit

Switch It Off Before They Find It: Five Default Loaders That Fail a UK Accessibility Audit

The 3am Phone Call You Don't Need: Quietly Measuring Script Failures in the Wild

The 3am Phone Call You Don't Need: Quietly Measuring Script Failures in the Wild