Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Include a print stylesheet

A print stylesheet is provided and correctly optimized for printed pages.

Utilities
Quick take
Typical fix time 30 min
  • Use @media print for print-specific styles
  • Hide navigation, ads, and interactive elements with .no-print
  • Use serif fonts at 12pt and optimize page breaks
  • Show full URLs for external links in printed output
Why it matters: Print stylesheets ensure content is readable when printed—removing digital clutter and optimizing for paper saves ink and improves accessibility.

Rule Details

A print stylesheet ensures web content is properly formatted when printed, removing digital-only elements and optimizing typography and layout for physical media.

Code Examples

<!-- Method 1: Separate print stylesheet -->
<link rel="stylesheet" href="print.css" media="print">
 
<!-- Method 2: All styles in one file -->
<link rel="stylesheet" href="styles.css">
 
<!-- Method 3: Combined with screen styles -->
<link rel="stylesheet" href="main.css" media="screen, print">

CSS Media Query

/* Method 1: Separate print styles */
@media print {
  /* Print-specific styles here */
}
 
/* Method 2: Import print styles */
@import url('print.css') print;
 
/* Method 3: Screen vs Print comparison */
@media screen {
  .print-only { display: none; }
}
 
@media print {
  .screen-only { display: none; }
  .print-only { display: block; }
}

Why It Matters

Print stylesheets ensure content is readable when printed—removing digital clutter and optimizing for paper saves ink and improves accessibility.

Complete Print Stylesheet

Essential Print CSS

/* print.css */
@media print {
  /* Reset and base styles */
  * {
    -webkit-print-color-adjust: exact !important;
    color-adjust: exact !important;
    print-color-adjust: exact !important;
  }
 
  html {
    font-size: 12pt;
    line-height: 1.4;
  }
 
  body {
    margin: 0;
    padding: 0;
    font-family: "Times New Roman", Times, serif;
    font-size: 12pt;
    line-height: 1.4;
    color: #000;
    background: #fff;
  }
 
  /* Page setup */
  @page {
    margin: 2cm;
    size: A4;
 
    @top-left {
      content: "Company Name";
      font-size: 10pt;
      color: #666;
    }
 
    @top-right {
      content: "Page " counter(page) " of " counter(pages);
      font-size: 10pt;
      color: #666;
    }
 
    @bottom-center {
      content: attr(title);
      font-size: 10pt;
      color: #666;
    }
  }
 
  /* Typography */
  h1, h2, h3, h4, h5, h6 {
    color: #000;
    page-break-after: avoid;
    break-after: avoid;
    font-weight: bold;
    margin-top: 0;
  }
 
  h1 { font-size: 18pt; margin-bottom: 12pt; }
  h2 { font-size: 16pt; margin-bottom: 10pt; }
  h3 { font-size: 14pt; margin-bottom: 8pt; }
  h4 { font-size: 12pt; margin-bottom: 6pt; }
 
  p {
    margin: 0 0 6pt 0;
    orphans: 3;
    widows: 3;
  }
 
  /* Lists */
  ul, ol {
    margin: 0 0 6pt 18pt;
    padding: 0;
  }
 
  li {
    margin-bottom: 3pt;
  }
 
  /* Tables */
  table {
    border-collapse: collapse;
    width: 100%;
    margin-bottom: 12pt;
    page-break-inside: avoid;
  }
 
  th, td {
    border: 1pt solid #000;
    padding: 4pt;
    text-align: left;
    vertical-align: top;
  }
 
  th {
    background: #f0f0f0;
    font-weight: bold;
  }
 
  /* Images */
  img {
    max-width: 100%;
    height: auto;
    page-break-inside: avoid;
  }
 
  /* Links */
  a {
    color: #000;
    text-decoration: underline;
  }
 
  a[href]:after {
    content: " (" attr(href) ")";
    font-size: 10pt;
    color: #666;
  }
 
  a[href^="mailto:"]:after {
    content: " (" attr(href) ")";
  }
 
  a[href^="tel:"]:after {
    content: " (" attr(href) ")";
  }
 
  /* Hide digital-only elements */
  .no-print,
  .screen-only,
  nav,
  .navigation,
  .sidebar,
  .ads,
  .advertisement,
  .social-media,
  .share-buttons,
  .comments,
  .footer-links,
  .breadcrumbs,
  .pagination,
  .search-form,
  .filters,
  .toolbar,
  .modal,
  .popup,
  .tooltip,
  .dropdown,
  .carousel-controls,
  .video-controls,
  button[type="submit"],
  input[type="button"],
  input[type="submit"],
  .btn,
  .button {
    display: none !important;
  }
 
  /* Show print-only elements */
  .print-only {
    display: block !important;
  }
 
  /* Page breaks */
  .page-break-before {
    page-break-before: always;
    break-before: page;
  }
 
  .page-break-after {
    page-break-after: always;
    break-after: page;
  }
 
  .page-break-inside-avoid {
    page-break-inside: avoid;
    break-inside: avoid;
  }
 
  /* Article/content specific */
  article,
  .content,
  .main-content {
    width: 100%;
    margin: 0;
    padding: 0;
    float: none;
  }
 
  /* Footer adjustments */
  footer {
    border-top: 1pt solid #ccc;
    padding-top: 6pt;
    margin-top: 12pt;
    font-size: 10pt;
  }
}

Framework-Specific Implementation

React Print Component

// components/PrintStyles.jsx
import { useEffect } from 'react'
 
function PrintStyles() {
  useEffect(() => {
    // Inject print styles dynamically
    const printStyles = `
      @media print {
        .no-print { display: none !important; }
        .print-only { display: block !important; }
 
        body { font-size: 12pt; }
        h1 { font-size: 18pt; }
        h2 { font-size: 16pt; }
 
        @page {
          margin: 2cm;
          size: A4;
        }
      }
    `
 
    const styleSheet = document.createElement('style')
    styleSheet.type = 'text/css'
    styleSheet.innerText = printStyles
    styleSheet.media = 'print'
    document.head.appendChild(styleSheet)
 
    return () => {
      document.head.removeChild(styleSheet)
    }
  }, [])
 
  return null
}
 
// components/PrintWrapper.jsx
function PrintWrapper({ children, title }) {
  const handlePrint = () => {
    window.print()
  }
 
  return (
    <div className="print-wrapper">
      <div className="no-print print-controls">
        <button onClick={handlePrint} className="btn btn-print">
          🖨️ Print this page
        </button>
      </div>
 
      <div className="print-content">
        <header className="print-only print-header">
          <h1>{title}</h1>
          <p>Printed on: {new Date().toLocaleDateString()}</p>
        </header>
 
        {children}
 
        <footer className="print-only print-footer">
          <p>© 2025 Company Name. All rights reserved.</p>
        </footer>
      </div>
    </div>
  )
}
 
// Usage
function ArticlePage() {
  return (
    <PrintWrapper title="Article Title">
      <PrintStyles />
 
      <article>
        <h1>Article Title</h1>
        <p>Article content...</p>
 
        <div className="no-print social-sharing">
          <button>Share on Twitter</button>
          <button>Share on Facebook</button>
        </div>
 
        <div className="print-only">
          <p>Visit our website at: https://example.com</p>
        </div>
      </article>
    </PrintWrapper>
  )
}

Next.js Print Implementation

// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document'
 
export default function Document() {
  return (
    <Html>
      <Head>
        <link rel="stylesheet" href="/css/print.css" media="print" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}
 
// components/PrintLayout.jsx
import Head from 'next/head'
 
function PrintLayout({ children, pageTitle }) {
  const printDate = new Date().toLocaleDateString()
 
  return (
    <>
      <Head>
        <style jsx>{`
          @media print {
            .print-header {
              display: block;
              border-bottom: 1pt solid #000;
              margin-bottom: 12pt;
              padding-bottom: 6pt;
            }
 
            .print-date {
              font-size: 10pt;
              color: #666;
            }
 
            @page {
              @top-left {
                content: "${pageTitle}";
              }
 
              @top-right {
                content: "Page " counter(page);
              }
            }
          }
 
          @media screen {
            .print-header {
              display: none;
            }
          }
        `}</style>
      </Head>
 
      <div className="print-header">
        <h1>{pageTitle}</h1>
        <p className="print-date">Printed: {printDate}</p>
      </div>
 
      {children}
    </>
  )
}
 
// Usage in pages
export default function BlogPost({ post }) {
  return (
    <PrintLayout pageTitle={post.title}>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
 
        <div className="no-print">
          <ShareButtons post={post} />
          <Comments postId={post.id} />
        </div>
      </article>
    </PrintLayout>
  )
}

Vue.js Print Directive

<!-- PrintableContent.vue -->
<template>
  <div class="printable-content">
    <div class="no-print print-controls">
      <button @click="printPage" class="btn-print">
        Print Page
      </button>
    </div>
 
    <div class="content" v-print-styles>
      <slot />
    </div>
 
    <div class="print-only print-footer">
      <p>Printed from: {{ currentUrl }}</p>
      <p>Date: {{ printDate }}</p>
    </div>
  </div>
</template>
 
<script>
export default {
  name: 'PrintableContent',
 
  computed: {
    currentUrl() {
      return window.location.href
    },
 
    printDate() {
      return new Date().toLocaleDateString()
    }
  },
 
  methods: {
    printPage() {
      window.print()
    }
  },
 
  directives: {
    printStyles: {
      inserted(el) {
        // Add print-specific classes
        el.classList.add('print-optimized')
      }
    }
  }
}
</script>
 
<style>
@media print {
  .print-optimized {
    font-family: 'Times New Roman', serif;
    font-size: 12pt;
    line-height: 1.4;
  }
 
  .print-optimized h1 {
    font-size: 18pt;
    margin-bottom: 12pt;
  }
 
  .print-optimized p {
    margin-bottom: 6pt;
  }
}
</style>

Advanced Print Features

CSS Page Rules

@media print {
  /* Different page layouts */
  @page :first {
    margin-top: 3cm;
 
    @top-left {
      content: "";
    }
 
    @top-center {
      content: "Document Title";
      font-size: 14pt;
      font-weight: bold;
    }
  }
 
  @page :left {
    margin-left: 3cm;
    margin-right: 2cm;
 
    @bottom-left {
      content: counter(page);
    }
  }
 
  @page :right {
    margin-left: 2cm;
    margin-right: 3cm;
 
    @bottom-right {
      content: counter(page);
    }
  }
 
  /* Named page layouts */
  @page chapter {
    margin: 3cm;
 
    @top-center {
      content: "Chapter " counter(chapter);
    }
  }
 
  @page appendix {
    margin: 2cm;
 
    @top-center {
      content: "Appendix";
    }
  }
 
  /* Apply named pages */
  .chapter {
    page: chapter;
    page-break-before: always;
  }
 
  .appendix {
    page: appendix;
    page-break-before: always;
  }
}
// print-handler.js
class PrintHandler {
  constructor() {
    this.beforePrintCallbacks = []
    this.afterPrintCallbacks = []
    this.setupEventListeners()
  }
 
  setupEventListeners() {
    window.addEventListener('beforeprint', () => {
      this.handleBeforePrint()
    })
 
    window.addEventListener('afterprint', () => {
      this.handleAfterPrint()
    })
 
    // For browsers that don't support beforeprint/afterprint
    if (window.matchMedia) {
      const mediaQueryList = window.matchMedia('print')
      mediaQueryList.addEventListener('change', (mql) => {
        if (mql.matches) {
          this.handleBeforePrint()
        } else {
          this.handleAfterPrint()
        }
      })
    }
  }
 
  handleBeforePrint() {
    // Execute before print callbacks
    this.beforePrintCallbacks.forEach(callback => callback())
 
    // Common before-print tasks
    this.expandCollapsedSections()
    this.showFullUrls()
    this.addPrintTimestamp()
  }
 
  handleAfterPrint() {
    // Execute after print callbacks
    this.afterPrintCallbacks.forEach(callback => callback())
 
    // Restore original state
    this.collapseExpandedSections()
    this.hideFullUrls()
    this.removePrintTimestamp()
  }
 
  expandCollapsedSections() {
    const collapsible = document.querySelectorAll('.collapsible[data-collapsed="true"]')
    collapsible.forEach(element => {
      element.setAttribute('data-print-expanded', 'true')
      element.setAttribute('data-collapsed', 'false')
    })
  }
 
  collapseExpandedSections() {
    const printExpanded = document.querySelectorAll('[data-print-expanded="true"]')
    printExpanded.forEach(element => {
      element.setAttribute('data-collapsed', 'true')
      element.removeAttribute('data-print-expanded')
    })
  }
 
  showFullUrls() {
    const links = document.querySelectorAll('a[href]')
    links.forEach(link => {
      if (!link.textContent.includes(link.href)) {
        const url = document.createElement('span')
        url.className = 'print-url'
        url.textContent = ` (${link.href})`
        link.appendChild(url)
      }
    })
  }
 
  hideFullUrls() {
    const printUrls = document.querySelectorAll('.print-url')
    printUrls.forEach(url => url.remove())
  }
 
  addPrintTimestamp() {
    const timestamp = document.createElement('div')
    timestamp.className = 'print-timestamp'
    timestamp.textContent = `Printed: ${new Date().toLocaleString()}`
    document.body.insertBefore(timestamp, document.body.firstChild)
  }
 
  removePrintTimestamp() {
    const timestamp = document.querySelector('.print-timestamp')
    if (timestamp) {
      timestamp.remove()
    }
  }
 
  onBeforePrint(callback) {
    this.beforePrintCallbacks.push(callback)
  }
 
  onAfterPrint(callback) {
    this.afterPrintCallbacks.push(callback)
  }
 
  print() {
    window.print()
  }
}
 
// Usage
const printHandler = new PrintHandler()
 
printHandler.onBeforePrint(() => {
  console.log('Preparing for print...')
  // Custom logic before printing
})
 
printHandler.onAfterPrint(() => {
  console.log('Print dialog closed')
  // Custom logic after printing
})

Testing Print Styles

Browser Testing

// print-style-tester.js
class PrintStyleTester {
  async testPrintStyles(url) {
    // Create a hidden iframe for testing
    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = url
    document.body.appendChild(iframe)
 
    return new Promise((resolve) => {
      iframe.onload = () => {
        const iframeDoc = iframe.contentDocument
        const printStyles = this.extractPrintStyles(iframeDoc)
 
        const results = {
          hasPrintStyles: printStyles.length > 0,
          printStylesheets: printStyles,
          hiddenElements: this.findHiddenElements(iframeDoc),
          pageBreaks: this.findPageBreaks(iframeDoc),
          typography: this.analyzeTypography(iframeDoc)
        }
 
        document.body.removeChild(iframe)
        resolve(results)
      }
    })
  }
 
  extractPrintStyles(doc) {
    const printStyles = []
 
    // Check for print media stylesheets
    const links = doc.querySelectorAll('link[rel="stylesheet"][media*="print"]')
    links.forEach(link => {
      printStyles.push({
        type: 'external',
        href: link.href,
        media: link.media
      })
    })
 
    // Check for @media print in style tags
    const styles = doc.querySelectorAll('style')
    styles.forEach(style => {
      if (style.textContent.includes('@media print')) {
        printStyles.push({
          type: 'inline',
          content: style.textContent
        })
      }
    })
 
    return printStyles
  }
 
  findHiddenElements(doc) {
    const hiddenSelectors = [
      '.no-print',
      '.screen-only',
      'nav',
      '.navigation',
      '.sidebar'
    ]
 
    const hiddenElements = []
    hiddenSelectors.forEach(selector => {
      const elements = doc.querySelectorAll(selector)
      if (elements.length > 0) {
        hiddenElements.push({
          selector,
          count: elements.length
        })
      }
    })
 
    return hiddenElements
  }
 
  findPageBreaks(doc) {
    const pageBreakSelectors = [
      '.page-break-before',
      '.page-break-after',
      '.page-break-inside-avoid'
    ]
 
    const pageBreaks = []
    pageBreakSelectors.forEach(selector => {
      const elements = doc.querySelectorAll(selector)
      if (elements.length > 0) {
        pageBreaks.push({
          selector,
          count: elements.length
        })
      }
    })
 
    return pageBreaks
  }
 
  analyzeTypography(doc) {
    const headings = doc.querySelectorAll('h1, h2, h3, h4, h5, h6')
    const paragraphs = doc.querySelectorAll('p')
 
    return {
      headings: headings.length,
      paragraphs: paragraphs.length,
      hasSerif: this.checkFontFamily(doc, 'serif'),
      fontSize: this.checkFontSizes(doc)
    }
  }
 
  checkFontFamily(doc, family) {
    const body = doc.body
    const computedStyle = window.getComputedStyle(body)
    return computedStyle.fontFamily.includes(family)
  }
 
  checkFontSizes(doc) {
    const body = doc.body
    const computedStyle = window.getComputedStyle(body)
    return {
      body: computedStyle.fontSize,
      root: computedStyle.fontSize
    }
  }
}
 
// Usage
const tester = new PrintStyleTester()
tester.testPrintStyles('https://example.com').then(results => {
  console.log('Print Style Analysis:', results)
})

Automated Testing with Puppeteer

// print-test.js
const puppeteer = require('puppeteer')
 
async function testPrintStyles(url) {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
 
  await page.goto(url)
 
  // Generate PDF to test print styles
  const pdf = await page.pdf({
    format: 'A4',
    margin: {
      top: '2cm',
      bottom: '2cm',
      left: '2cm',
      right: '2cm'
    },
    printBackground: true
  })
 
  // Analyze print styles
  const printAnalysis = await page.evaluate(() => {
    const printStylesheets = Array.from(
      document.querySelectorAll('link[rel="stylesheet"][media*="print"]')
    ).map(link => ({
      href: link.href,
      media: link.media
    }))
 
    const printMediaQueries = Array.from(document.querySelectorAll('style'))
      .filter(style => style.textContent.includes('@media print'))
      .length
 
    const hiddenElements = document.querySelectorAll('.no-print, .screen-only').length
    const printOnlyElements = document.querySelectorAll('.print-only').length
 
    return {
      hasPrintStyles: printStylesheets.length > 0 || printMediaQueries > 0,
      printStylesheets,
      printMediaQueries,
      hiddenElements,
      printOnlyElements
    }
  })
 
  await browser.close()
 
  return {
    analysis: printAnalysis,
    pdfGenerated: pdf.length > 0
  }
}
 
// Usage
testPrintStyles('https://example.com').then(results => {
  console.log('Print Test Results:', results)
})

Best Practices

  1. Always Provide: Include print styles for all public-facing content
  2. Hide Unnecessary: Remove navigation, ads, and interactive elements
  3. Optimize Typography: Use serif fonts and appropriate sizes (12pt)
  4. Page Breaks: Control where content breaks across pages
  5. URLs in Links: Show full URLs for external links
  6. Test Regularly: Verify print styles work across browsers
  7. Performance: Keep print CSS lightweight and efficient
  8. Accessibility: Ensure printed content remains accessible

Common Print CSS Patterns

Invoice/Receipt Layout

@media print {
  .invoice {
    width: 100%;
    margin: 0;
    padding: 0;
  }
 
  .invoice-header {
    border-bottom: 2pt solid #000;
    margin-bottom: 12pt;
    padding-bottom: 6pt;
  }
 
  .invoice-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 12pt;
  }
 
  .invoice-table th,
  .invoice-table td {
    border: 1pt solid #000;
    padding: 6pt;
    text-align: left;
  }
 
  .invoice-total {
    font-weight: bold;
    font-size: 14pt;
    text-align: right;
    border-top: 2pt solid #000;
    padding-top: 6pt;
  }
}

Article/Blog Post Layout

@media print {
  .article {
    max-width: none;
    margin: 0;
  }
 
  .article-header {
    margin-bottom: 18pt;
    page-break-after: avoid;
  }
 
  .article-title {
    font-size: 20pt;
    font-weight: bold;
    margin-bottom: 6pt;
  }
 
  .article-meta {
    font-size: 10pt;
    color: #666;
    border-bottom: 1pt solid #ccc;
    padding-bottom: 6pt;
  }
 
  .article-content p {
    text-align: justify;
    margin-bottom: 8pt;
  }
 
  .article-content blockquote {
    border-left: 2pt solid #000;
    padding-left: 12pt;
    margin: 12pt 0;
    font-style: italic;
  }
}

Verification

Automated Checks

  • Use browser or CI tooling where applicable to verify the fix.

Manual Checks

  • Confirm the rule in the final rendered output or runtime behavior.
  • Test one primary path and one edge case affected by the change.
  • Re-check shared abstractions so the fix is applied consistently.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Verify that a print stylesheet is provided and properly optimized for printed pages with appropriate typography, layout, and hidden elements.

Fix

Auto-fix issues

Create a print stylesheet that removes unnecessary elements, optimizes typography for print, and ensures content is readable when printed.

Explain

Learn more

Explain how print stylesheets improve user experience for printed pages by optimizing layout, removing digital-only elements, and ensuring readability.

Review

Code review

Review stylesheets, component styles, and responsive states related to Include a print stylesheet. Flag exact selectors, declarations, or breakpoints that violate the rule in the rendered UI.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Chrome DevTools
developer.chrome.comTool

Rules that often go hand-in-hand with this one.

Avoid embedded and inline CSS

Embedded and inline CSS are avoided except for critical CSS and performance optimization.

CSS
Use a CSS reset or normalize stylesheet

A CSS reset or normalize is used to ensure consistent styling across browsers.

CSS
Use :has() to style parent elements based on their descendants

Use the CSS :has() relational pseudo-class to select and style an element based on what it contains, replacing JavaScript DOM manipulation for many common styling scenarios.

CSS
Use @layer to manage CSS cascade order explicitly

CSS Cascade Layers (@layer) are used to give the codebase explicit, predictable control over specificity and cascade order, eliminating the need to fight specificity with !important.

CSS

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385