EmpireUI
Get Pro
← Blog7 min read#print-css#media-queries#css

Print Stylesheets in 2026: @media print Best Practices

Print stylesheets are ignored until a client needs a PDF. Here's how @media print works in 2026, with real CSS, Tailwind v4 tricks, and layout patterns that won't break on paper.

A printed document on a desk next to a laptop showing CSS code on screen

Why Print CSS Still Matters in 2026

Honestly, most developers ship a product, forget print stylesheets exist, and then get a panicked Slack message three weeks before a sales demo: "the invoice prints completely black with sidebar nav on every page." It's one of those things that feels optional right up until it isn't.

Here's where the market is right now: PDF generation via headless Chrome (Puppeteer, Playwright, wkhtmltopdf's successor tools) has overtaken server-side rendering for most SaaS invoice and report workflows. But that doesn't mean @media print is dead. It means your print stylesheet is now being parsed by a headless browser running at 96dpi — and it needs to be right.

The other reality is accessibility. Screen readers aside, users who print pages for offline reference — medical forms, legal documents, boarding passes, contracts — need a clean output. Sending a printed page that's half navbar and half sidebar tells the user you didn't care. And they notice.

The @media print Cascade: How Browsers Actually Handle It

There's a common misunderstanding: developers think @media print styles replace everything. They don't. The cascade still runs. Your base styles apply, then your print media query overrides. That means if you define body { background: rgba(15, 15, 20, 1); } in your dark theme and forget to reset it in print, the page prints as a solid black rectangle. Fun debugging session.

The correct mental model is that you're writing overrides. You reset backgrounds to white (#ffffff), set text to black (#000000), collapse navigational elements to display: none, and reconfigure layout from flex/grid to block flow. Everything else inherits from your existing rules, for better or worse.

One thing that trips up Tailwind users specifically: utility classes generate scoped media queries internally. Tailwind v4.0.2 introduced first-class print variant support via print:hidden, print:block, and related utilities. If you're already on v4, those just work. If you're still on v3, you're writing the query by hand in a globals CSS file.

Core @media print Reset Pattern

Here's the stylesheet I actually use as a starting point on client projects. It's not minimal to the point of useless, but it's also not 400 lines of edge-case bloat.

@media print {
  *,
  *::before,
  *::after {
    background: transparent !important;
    color: #000000 !important;
    box-shadow: none !important;
    text-shadow: none !important;
  }

  body {
    font-size: 12pt;
    line-height: 1.5;
    font-family: Georgia, 'Times New Roman', serif;
    margin: 0;
    padding: 0;
  }

  /* Hide everything that's screen-only */
  nav,
  aside,
  header,
  footer,
  .no-print,
  [data-print="hidden"] {
    display: none !important;
  }

  /* Expand links to show URLs */
  a[href]::after {
    content: ' (' attr(href) ')';
    font-size: 10pt;
    color: #555555 !important;
  }

  /* Avoid orphaned headings */
  h1, h2, h3, h4 {
    page-break-after: avoid;
    break-after: avoid;
  }

  /* Keep table rows together where possible */
  tr,
  img {
    page-break-inside: avoid;
    break-inside: avoid;
  }

  /* Force full-width layout */
  .container,
  main,
  article {
    width: 100% !important;
    max-width: 100% !important;
    padding: 0 !important;
    margin: 0 !important;
  }
}

Notice the !important usage. It's justified here because print contexts are a hard reset — you genuinely want to override everything including inline styles that might have snuck in from a component library. Don't feel bad about it.

Page Size, Margins, and @page Rules

The @page at-rule controls the printed page itself, separate from element styles. Most developers skip it and accept browser defaults (usually A4 or Letter with 1cm margins). That's fine for internal tools. For anything client-facing, you'll want to control it explicitly.

@page {
  size: A4 portrait;
  margin: 20mm 15mm 25mm 15mm;
}

@page :first {
  margin-top: 30mm; /* extra space for cover-page content */
}

@page :left {
  margin-left: 25mm;
}

@page :right {
  margin-right: 25mm;
}

The :left and :right selectors are for double-sided (duplex) printing — binding margins on the inner edge. If your app generates reports for legal or finance clients, they'll notice the difference between a document that looks right bound versus one that's clearly a web page that was printed.

One browser support note: @page size declarations work reliably in Chrome 112+ and Firefox 110+. Safari's support is still partial as of 2026 — size is implemented but named sizes like A4 sometimes get ignored. Test in all three if your users are on mixed platforms.

Handling Tailwind v4 Print Utilities

If your project runs Tailwind v4.0.2 or later, you can handle most print layout purely in JSX without touching a CSS file. The print: variant prefix works the same as dark: or hover:. You'll use it to toggle visibility and reconfigure layout columns.

// Example: Invoice component with print-aware layout
export function InvoiceLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex gap-8 print:block print:gap-0">
      {/* Sidebar hidden on print */}
      <aside className="w-64 shrink-0 print:hidden">
        <NavigationMenu />
      </aside>

      {/* Main content expands to full width */}
      <main className="flex-1 min-w-0 print:w-full">
        {children}
      </main>
    </div>
  );
}

// Table that avoids bad page breaks
export function InvoiceTable({ rows }: { rows: LineItem[] }) {
  return (
    <table className="w-full text-sm print:text-xs">
      <tbody>
        {rows.map((row) => (
          <tr
            key={row.id}
            className="border-b border-gray-200 print:break-inside-avoid"
          >
            <td className="py-2 print:py-1">{row.description}</td>
            <td className="py-2 print:py-1 text-right">{row.amount}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

The print:break-inside-avoid utility maps directly to break-inside: avoid in the generated CSS. It's one of those things that Tailwind v4 got right — the naming matches the property closely enough that you don't need to memorize a separate abstraction.

When you're thinking about how these utilities interact with your spacing system, keep in mind that print contexts ignore viewport-based spacing. Anything defined in vw or vh units will collapse or overflow. Stick to fixed units (pt, mm) or percentages in print overrides.

Typography and Readability on Paper

Screen typography and print typography have genuinely different requirements. On screen, 16px body text is standard. On paper at 96dpi, 12pt (which is 16px at screen resolution) can look enormous. Most print stylesheets drop to 10pt or 11pt for body copy and use a serif face — Georgia or Times — because serifs are physically easier to track across a printed line.

Does every project need a full serif override? No. If you're printing a developer dashboard, monospace is probably right for data. But for prose-heavy documents — reports, proposals, summaries — font-family: Georgia, 'Times New Roman', serif at 12pt with line-height: 1.6 reads cleanly at A4 and Letter sizes.

Color contrast matters here too, but differently than on screen. Your WCAG accessibility guide considerations for screen contrast ratios don't map 1:1 to print. Paper printing actually tolerates lower contrast in large text and benefits from pure black-on-white for body. If you've been using color: #374151 (Tailwind's gray-700) for body text, that's fine for screens but can look slightly washed out on some laser printers. #000000 or #111111 is safer.

Common Print Bugs and How to Track Them Down

The fastest way to test print output without burning paper: Chrome DevTools, then Ctrl+Shift+P (or Cmd+Shift+P on Mac), search "rendering", open the Rendering panel, and set "Emulate CSS media type" to "print". That applies your @media print rules live in the browser. You'll catch the obvious layout breaks in seconds.

The bugs you'll see most often fall into a few categories. First, fixed-position elements. Navbars with position: fixed don't vanish at print time — they overlay every page. The fix is simple (display: none) but easy to forget. Second, background images and gradients. Browsers strip most background visuals by default (to save ink), so any information conveyed only through background styling disappears. Always use foreground content for data.

Third, and this one's subtle: elements with overflow: hidden can clip content that extends beyond their screen-sized bounds. A sidebar panel that's 280px wide and 600px tall might contain content that extends further when the print cascade removes its sibling columns and reflowing occurs. Set overflow: visible on containers in print mode.

If you're building a color system with semantic tokens, it's worth adding explicit print tokens — --color-text-print: #000000, --color-background-print: #ffffff — so the reset lives in your token layer rather than scattered across component stylesheets.

Print Stylesheets in a Design System Context

When you're working with a component library (whether that's Empire UI, your own system documented in Storybook, or something else), print support needs to be a documented property of each component, not an afterthought. Does this <Modal> hide itself on print? Does this <DataTable> handle page breaks? These are questions that belong in the component spec.

The pattern I'd recommend: add a data-print attribute API to components that have print-specific behavior. Something like data-print="hidden" for components that should vanish, data-print="visible" to force visibility when a parent hides it. Then your global print stylesheet targets these attributes. It keeps print logic out of component-specific CSS while still giving consumers control.

One final thing worth considering: if your design system ever surfaces icon systems with SVG icons, verify that those SVGs have explicit fill or stroke colors set rather than relying on currentColor cascading from text. Some print drivers handle SVG color inheritance inconsistently. Hardcoding fill="#000000" in print-specific icon variants is overkill for most projects, but if your icons are disappearing on client printers, that's where to look.

FAQ

Why does my navbar print on every page even though I added display: none?

Almost certainly because it has position: fixed. Fixed elements are removed from normal flow but they still render in print — on every page. You need both display: none and position: static (or just display: none alone) in your @media print block to make them disappear completely.

Does Tailwind v4 generate @media print rules automatically?

Not automatically, but Tailwind v4.0.2+ supports the print: variant prefix natively. Classes like print:hidden, print:block, print:text-black, and print:break-inside-avoid work out of the box without any plugin or custom config. They only generate CSS if you actually use those classes in your markup.

My background colors and gradients disappear when printing. Is that a CSS bug?

No, that's intentional browser behavior. Browsers suppress background-color and background-image by default during print to save ink. You can force them with background-print-color-adjust: exact (standardized) or the older -webkit-print-color-adjust: exact. Add both to be safe. Note that this also forces any colored table headers or progress bars to print as intended.

How do I show the actual URL when links are printed?

Use a ::after pseudo-element with the attr() function: a[href]::after { content: ' (' attr(href) ')'; font-size: 10pt; }. You'll probably also want to exclude internal links and anchor links: a[href^='#']::after, a[href^='/']::after { content: none; } so you're not printing meaningless fragment identifiers.

What's the difference between page-break-after and break-after in modern CSS?

page-break-after, page-break-before, and page-break-inside are the old CSS2 properties. The modern replacements are break-after, break-before, and break-inside from the CSS Fragmentation spec (Level 3). They work the same for print but break-* also applies to multi-column and flex fragmentation contexts. Write both for compatibility: page-break-after: avoid; break-after: avoid;

Can I test print output without actually printing or generating a PDF?

Yes. In Chrome DevTools, open the command palette (Ctrl+Shift+P / Cmd+Shift+P), search for 'Rendering', and in that panel set 'Emulate CSS media type' to 'print'. This applies @media print rules live to the current page without generating a print dialog. Firefox has a similar toggle under Developer Tools > Responsive Design Mode > print media emulation.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Building Design Systems That Scale: Engineering Guide 2026Accessibility-First Design Systems: WCAG 2.2 in Every ComponentDark Mode UI Design: Principles, Pitfalls and Best PracticesReact Animation Best Practices: Performance, Accessibility, APIs