CSSで、印刷するときだけレイアウトを変える方法

​​CSS​HTML,Playwright

印刷するときだけレイアウトを変えるには、印刷用のスタイルを @media print の中か、media="print" を付けた link 要素で読み込む CSS に書きます。メニューを消す、カードの列の数を変える、決まった場所でページを分ける、といった指定を、画面用の CSS には手を入れずに足せます。

列の数は、画面用のメディアクエリ(min-width など)に任せず、印刷用の CSS の中で直接決めるのが確実です。Chromium で PDF に出力して調べたところ、印刷のときに min-width が比べる幅は、@page で決めた紙の大きさでは変わらず、PDF を出力する側(Playwright の page.pdf())に渡した紙の設定で変わりました。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。印刷の結果は、Playwright の page.pdf() で Chromium に PDF を出力させ、pdftotext で文字を取り出したり、画像にしたりして調べました。page.pdf() は印刷用の CSS(print)で PDF を作ります。@page で決めた紙の大きさを使わせるため、preferCSSPageSize: true を付けています。このオプションは、@page の大きさを page.pdf() の紙の設定より優先させるもので、付けないと紙は既定の Letter になります。印刷ダイアログや印刷プレビューを開いての印刷、実際のプリンター、Firefox と Safari、Chrome の DevTools の操作は試していません。

例にした旅のしおりのページ

例にしたのは、3日間の旅の予定を載せたページです。幅が 900px 以上の画面では、左に日ごとのメニューを置き、行き先のカードを3列に並べます。いちばん上には、スクロールしても画面に残るヘッダーがあり、「印刷する」のボタンを置いています。

上に濃い水色のヘッダーがあり、左に「旅のしおり」、右に「印刷する」のボタンがある。その下の左に「1日目」「2日目」「3日目」のリンクが縦に並び、右に「海辺の町をめぐる3日間」の見出しと、日ごとの見出しの下に行き先のカードが3列で並んでいる
画面で見たときのページ(幅 1000px)

HTML は、日ごとに section.day を分け、その中の div.spots にカードを入れています。CSS は、画面用の style.css と、印刷用の print.css の2つを読み込みます。print.css の link 要素には、media 属性で print を指定しています。

html
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="print.css" media="print">
html
<header class="site-header">
  <span class="logo">旅のしおり</span>
  <button type="button" onclick="window.print()">印刷する</button>
</header>
<div class="layout">
  <nav class="side-nav">
    <a href="#day1">1日目</a>
    <a href="#day2">2日目</a>
    <a href="#day3">3日目</a>
  </nav>
  <main>
    <h1>海辺の町をめぐる3日間</h1>
    <section class="day" id="day1">
      <h2>1日目 港と市場</h2>
      <div class="spots">
        <article class="spot"><h3>朝市</h3><p>7:00〜9:00。港の前の通りで、とれたての魚や野菜が並びます。</p></article>
        <!-- カードが続く -->
      </div>
    </section>
    <section class="day" id="day2">…</section>
    <section class="day" id="day3">…</section>
  </main>
</div>

画面用の style.css は次のとおりです。ふだんはカードを1列にし、幅が 900px 以上のときだけ、メニューを左に置いてカードを3列にしています。

css
body { margin: 0; font-family: "Noto Sans CJK JP", sans-serif; color: #1f2a30; background: #eefafd; }
.site-header { position: sticky; top: 0; display: flex; justify-content: space-between; align-items: center;
  padding: 12px 20px; background: #067a98; color: #fff; }
.site-header button { font: inherit; padding: 6px 14px; }
.layout { display: grid; grid-template-columns: 1fr; gap: 24px; padding: 20px; }
.side-nav { display: flex; gap: 12px; }
.side-nav a { color: #067a98; }
.spots { display: grid; grid-template-columns: 1fr; gap: 12px; }
.spot { background: #fff; border: 1px solid #00c8e8; border-radius: 8px; padding: 4px 14px; }
.spot h3 { margin: 8px 0 0; font-size: 16px; }
.spot p { margin: 6px 0 10px; font-size: 14px; line-height: 1.6; }

/* 幅が 900px 以上の画面では、メニューを左に置き、カードを3列にする */
@media (min-width: 900px) {
  .layout { grid-template-columns: 160px 1fr; }
  .side-nav { flex-direction: column; }
  .spots { grid-template-columns: repeat(3, 1fr); }
}

印刷用の CSS で、消すものと並べ方を決める

print.css は、link 要素に media="print" を付けて読み込んでいるので、印刷するときだけ使われます。中身は次のとおりです。

css
/* この中のスタイルは、印刷するときだけ使われる(link の media="print") */
.site-header, .side-nav { display: none; }
body { background: #fff; }
.layout { display: block; padding: 0; }
/* カードは、紙の幅に関係なく2列にする */
.spots { grid-template-columns: repeat(2, 1fr); }
/* カードの途中でページを分けない */
.spot { break-inside: avoid; }
/* 2日目からは、新しいページから始める */
.day + .day { break-before: page; }

@page {
  size: A4;
  margin: 15mm;
}

紙の上では押せない「印刷する」のボタンがあるヘッダーと、メニューは display: none で消しています。画面用のグリッド(メニューと本文の2列)もやめて、本文を紙の幅いっぱいに広げました。カードは、紙の幅に合うように2列にしています。@page では、紙の大きさを A4 に、余白を上下左右 15mm にしました。

この CSS で PDF を出力すると、A4 の紙が3枚になりました。次の出力は、各ページの大きさと、PDF から取り出した最初の3行の文字です。「 | 」は行の区切りで、「 / 」は同じ行の中で横に離れて置かれた文字の間を表します。ヘッダーとメニューの文字は、どのページにも出ていません。

text
--- out/print.pdf ページ数 3
[1ページ] 594.96 x 841.92 pt : 海辺の町をめぐる3日間 | 1日目 港と市場 | 朝市 / 灯台
[2ページ] 594.96 x 841.92 pt : 2日目 島へ渡る | フェリー / 島の神社 | 8:30 発。島までは船で40分です。 / 9:30〜10:30。石段の上に小さな神社がありま
[3ページ] 594.96 x 841.92 pt : 3日目 町歩き | 古い商店街 / 美術館 | 9:00〜11:00。木造の店が並ぶ通りを歩きます。 / 11:30〜13:00。海を描いた絵を集めた美術館で

「朝市 / 灯台」のように、2つのカードの文字が同じ行に出ているのは、カードが2列に並んでいるためです。

A4 の紙の1ページ目。上に「海辺の町をめぐる3日間」と「1日目 港と市場」の見出しがあり、その下に水色の枠のカードが2列で並ぶ。1行目が「朝市」「灯台」、2行目が「海鮮食堂」「水族館」、3行目が「夕日の丘」だけで、ページの下半分は空いている。ヘッダーとメニューは出ていない
印刷用の CSS で PDF にした1ページ目

1つの CSS ファイルにまとめる書き方

印刷用のスタイルは、ファイルを分けずに、画面用の CSS の中に @media print で書くこともできます。

css
/* style.css の最後に足す */
@media print {
  .site-header, .side-nav { display: none; }
  body { background: #fff; }
  .layout { display: block; padding: 0; }
  .spots { grid-template-columns: repeat(2, 1fr); }
  .spot { break-inside: avoid; }
  .day + .day { break-before: page; }
}
@page { size: A4; margin: 15mm; }

print.css の中身をこの形で style.css の最後に足し、link を1つにしたページでも PDF を出力しました。print.css を分けた場合と比べると、全ページの文字と大きさが同じでした。どちらで書くかは、管理しやすいほうで選べます。

@media print は、画面用の CSS の後ろに書きます。MDN の印刷のガイドにあるとおり、印刷用のスタイルにも詳細度と順番の規則がそのまま当てはまります。.spots の列の数は、画面用の style.css にも同じ詳細度で書いてあるので、印刷用の指定を後ろに置かないと上書きできません。

印刷のときの min-width は、@page ではなく出力側の紙の設定で決まった

print.css を空にして PDF を出力すると、どうなるかも試しました。ブラウザの窓の幅は 1200px にしています。

Letter の紙の1ページ目。上に「旅のしおり」と「印刷する」のボタン、その下に「1日目 2日目 3日目」のリンクが横に並ぶ。カードは1列で、紙の幅いっぱいに縦に並んでいる
印刷用の CSS を空にして PDF にした1ページ目

ヘッダーとメニューがそのまま出ただけでなく、カードは3列ではなく1列になりました。メニューも、左ではなく上に横並びです。窓の幅は 1200px でも、@media (min-width: 900px) に当てはまっていません。

MDN の width のメディア特性の説明には、印刷のようにページに分けて出すときは、ビューポートではなくページの箱の幅を調べる、とあります。そこで、min-width を 1px 刻みで並べたスタイルを用意して、当てはまった最大の値を PDF に文字で出させました。紙の決め方を変えた結果は次のとおりです。

text
@page なし、page.pdf() の指定なし
  紙 612 x 792 pts (letter) / 当てはまった最大の min-width と向き: 幅 816
@page なし、page.pdf({ format: "A4" })
  紙 595.92 x 842.88 pts (A4) / 当てはまった最大の min-width と向き: 幅 794
@page { size: A4; margin: 15mm }、preferCSSPageSize
  紙 594.96 x 841.92 pts (A4) / 当てはまった最大の min-width と向き: 幅 816
@page { size: A4 landscape; margin: 15mm }、preferCSSPageSize
  紙 841.92 x 594.96 pts (A4) / 当てはまった最大の min-width と向き: 幅 816
@page { size: A4; margin: 0 }、preferCSSPageSize
  紙 594.96 x 841.92 pts (A4) / 当てはまった最大の min-width と向き: 幅 816
@page なし、page.pdf({ format: "A4", landscape: true })
  紙 842.88 x 595.92 pts (A4) / 当てはまった最大の min-width と向き: landscape 幅 1124
@page なし、page.pdf({ format: "A4", margin: 15mm })
  紙 595.92 x 842.88 pts (A4) / 当てはまった最大の min-width と向き: 幅 682

page.pdf() の紙の設定を変えると、幅も変わりました。何も指定しないときの紙は Letter(横 8.5インチ)で、幅は 816px です。A4 にすると 794px、A4 の横向きにすると 1124px で、orientation: landscape にも当てはまりました。page.pdf() で余白を 15mm にすると、幅は 682px に減りました。

一方、@page の size で紙を A4 にしたときは、PDF の紙は A4 になったのに、幅は Letter と同じ 816px のままでした。size: A4 landscape で横向きにしても、@page の余白を変えても、幅は変わりません。MDN の説明からは @page で決めたページの大きさで判断されそうに読めますが、この検証ではそうなりませんでした。

この結果では、@page で紙を決めていても、min-width が見る幅は page.pdf() に渡した紙の設定のほうで決まっていました。この記事の出力(@page で A4、page.pdf() の紙は既定の Letter)では幅が 816px なので、900px に届かず、画面用の3列のレイアウトにはなりません。page.pdf() で A4 の横向きにしたときは 1124px で、900px を超えます。印刷のときの列の数を画面用のブレークポイントに任せると、出力する側の紙の設定しだいで変わってしまいます。そのため、上の print.css では .spots の列の数を repeat(2, 1fr) と直接書いています。

2日目からは新しいページで始める

日ごとに紙を分けるため、section.day の前でページを分けています。break-before: page は、その要素の直前で必ずページを分ける指定です。

ここで .day すべてに付けず、.day + .day(前に .day がある .day)にしているのは、1日目の前でページが分かれないようにするためです。.day { break-before: page; } にして出力すると、4ページになりました。最初の2ページは次のとおりです。

text
--- out/all-break.pdf ページ数 4
[1ページ] 594.96 x 841.92 pt : 海辺の町をめぐる3日間
[2ページ] 594.96 x 841.92 pt : 1日目 港と市場 | 朝市 / 灯台 | 7:00〜9:00。港の前の通りで、とれたての魚や / 10:00〜11:30。岬の先にある白い灯台です。上

.day すべてに付けると、1日目の section の前でもページが分かれます。その前にはページの題の「海辺の町をめぐる3日間」しかないので、1ページ目は題だけになりました。

break-before を外すと、3日分が2ページに詰まって出ます。このとき、カードに付けた break-inside: avoid が効いてきます。要素の途中でページを分けないようにする指定です。break-inside も外して出力すると、3日目の「駅」のカードが、1ページ目の最後と2ページ目の先頭に分かれました。

PDF の1ページ目の下と2ページ目の上をつなげた画像。1ページ目の最後に「駅」のカードがあり、見出しと「15:00 発の電車で帰ります。」は1ページ目に出ているが、カードの枠の下の辺は1ページ目の終わりで途切れている。2ページ目の先頭には、カードの枠の下の辺だけが出ている
break-inside を外したとき、カードの枠が2ページに分かれた

2ページ目には文字がなく、カードの枠の下の辺だけが出ていました。break-inside: avoid を付けたまま break-before だけを外したときは、「駅」のカードがまるごと2ページ目に移りました。

text
--- out/no-break.pdf ページ数 2
[1ページ] 594.96 x 841.92 pt : 海辺の町をめぐる3日間 | 1日目 港と市場 | 朝市 / 灯台
[2ページ] 594.96 x 841.92 pt : 駅 | 15:00 発の電車で帰ります。
--- out/no-break-no-avoid.pdf ページ数 2
[1ページ] 594.96 x 841.92 pt : 海辺の町をめぐる3日間 | 1日目 港と市場 | 朝市 / 灯台
[2ページ] 594.96 x 841.92 pt : (文字なし)

上が break-inside: avoid を付けたまま、下が外したときの結果です。

画面で印刷用のレイアウトを確かめるときの注意

印刷のたびにプレビューを開かなくても、Chrome の DevTools で印刷用の CSS を画面に当てられます。DevTools で CSS のメディアの種類を切りかえる機能の説明によると、「Rendering」タブの「Emulate CSS media type」で「print」を選びます。

ここでは、Playwright の page.emulateMedia({ media: 'print' }) でメディアの種類を print に切りかえて調べました。窓の幅は 1000px です。

text
画面(幅 1000px): {
  print: false,
  w900: true,
  header: 'flex',
  columns: '250.656px 250.672px 250.672px'
}
emulateMedia({ media: "print" }) の後: { print: true, w900: true, header: 'none', columns: '494px 494px' }

print のメディアクエリに当てはまるようになり、ヘッダーは消えて、カードは2列になりました。ただし、(min-width: 900px) は当てはまったままです。emulateMedia で変わったのはメディアの種類だけで、min-width は窓の幅で判断されていました。同じページを PDF に出力したときは、前の節のとおり幅は 816px で、900px には届きません。

print.css のように列の数を直接書いておけば、emulateMedia で画面に当てたときも、PDF に出力したときも、同じ2列になりました。画面用のブレークポイントに頼った部分は、画面に印刷用の CSS を当てただけでは、紙の上と同じ結果になるとは限りません。

​Posted by 86