Printing

Deck's own summary is the right framing: real work still gets printed. Invoices, packing slips, purchase orders and reports all end up on paper or in a PDF, so this is a real stylesheet and not an afterthought. Most of it happens without you doing anything; this page is what it does, and the nine classes you use to steer it.

Where the print rules live

Two places, and knowing both saves an afternoon. The main sheet is src/99-print.css. There is a second @layer deck.print block at the end of src/24-media.css, holding the print rules for the media and messaging components so they sit beside the components themselves.

So "is there a print rule for X" is a question about the whole stylesheet, not about one file. .video, .chat, .drawer, .editor-toolbar, .masonry, .lazy, .qr, .mega, .banner, .speed-dial and the carousel controls are all handled in the second file.

What happens without you asking

  • The light palette is forced. color-scheme: light only and every surface token overridden to white, text to black, lines to grey. Dark mode is a screen idea, and printing a dark theme wastes toner and produces grey text.
  • Shadows, blurs and animation are removed from everything, with a universal selector. They cost ink and convey nothing on paper.
  • Deliberate fills are kept. Badges, chips, chart series, donuts, icon tiles and avatars get print-color-adjust: exact, because for those the colour is the content. Everything else loses its background the way the browser intends.
  • Chrome is dropped. The tab bar, floating action button, back-to-top, toast region, skip link, theme dock, nav links, sidebar, date picker, combo list, menus, data-grid toolbar, sheet grip, pagination, segmented controls and tabs all disappear — along with anything you have marked .no-print.
  • Link targets are printed. A link whose href starts with http gets its address appended in brackets. In-page anchors, javascript: links, .btn and anything marked .no-print-url are exempt.
  • Type is resized for paper. The body drops to 10.5pt with a 1.4 line height, and the heading scale is re-expressed in points rather than rem.
  • Headings do not end a page. break-after: avoid on every heading level, and break-inside: avoid on paragraphs, list items, figures, .stat and .timeline-item.
  • Dialogs print only if open, and then as a block in the flow rather than as an overlay.

Tables get the most work

Which is right, because a table is what people actually print. Three things happen that are worth knowing about:

  • The header repeats on every page. display: table-header-group on thead — so a fifty-row invoice keeps its column headings on page three. The footer gets table-footer-group to match.
  • Rows do not split. break-inside: avoid on every tr.
  • The mobile card fallback is undone. This is the clever one. .table-stack and .dg-cards turn a table into a stack of cards on a narrow screen — and a print sheet is a narrow viewport, so without intervention every printed table would come out as cards. The print rules put the table back: display: table, real header groups, and td::before { content: none } to remove the injected labels.

The data grid gets the same treatment plus a few of its own — sticky cells become position: static, the scroll wrapper loses its height cap, and cells are allowed to wrap rather than being clipped. A printed data grid is a table again.

Forms print as forms

A form on paper is usually something to be filled in by hand, and Deck treats it that way. Inputs, textareas and selects keep a visible grey border on white; the textarea loses its height cap so a long value prints in full; the select loses its dropdown arrow.

Buttons and .form-actions are removed — you cannot press a button on paper — with one exception: .btn.print-keep survives, bordered, for a button whose label is part of the document.

Checkboxes and radios are given exact colour rendering, so an unchecked box prints as an empty box and a checked one prints filled black rather than both vanishing into white.

Print this page and the button row disappears; the field and the tick do not
<div class="stack stack-3" style="max-inline-size:24rem"> <div class="field"> <label class="label" for="dx-pr-name">Name</label> <input class="input" id="dx-pr-name"> </div> <label class="check"> <input type="checkbox" checked> <span>I have read the terms</span> </label> <div class="form-actions"> <button type="button" class="btn btn-primary">Submit</button> </div> </div>
<div class="stack stack-3" style="max-inline-size:24rem">
  <div class="field">
    <label class="label" for="dx-pr-name">Name</label>
    <input class="input" id="dx-pr-name">
  </div>
  <label class="check">
    <input type="checkbox" checked>
    <span>I have read the terms</span>
  </label>
  <div class="form-actions">
    <button type="button" class="btn btn-primary">Submit</button>
  </div>
</div>

The nine classes

  • .page-break — break-before: page. Start this element on a new sheet.
  • .page-break-after — the same, after.
  • .keep-together — break-inside: avoid. Keep a block whole, for a signature panel or a total.
  • .no-print — hide on paper.
  • .print-only — the inverse: display: none on screen, revealed when printing. For a signature line, a reference number, or terms that would be clutter on screen.
  • .no-print-url — suppress the appended address on one link. It is .no-print-url::after, so it goes on the <a> itself.
  • .print-keep — on a .btn, keep it visible.
  • .print-header and .print-footer — position: fixed blocks that repeat on every printed page.
Use your browser's print preview to see the other half of this example

Visible on screen and on paper.

Screen only — a hint, a control, a nudge.

Signature

This block will not be split across two pages.

<div class="card stack stack-3" style="max-inline-size:26rem"> <p>Visible on screen and on paper.</p> <p class="no-print text-muted">Screen only — a hint, a control, a nudge.</p> <p class="print-only">Paper only — reference ORD-4417-QK, printed from example.com.</p> <div class="keep-together stack stack-1"> <strong>Signature</strong> <p class="text-muted">This block will not be split across two pages.</p> </div> </div>
<div class="card stack stack-3" style="max-inline-size:26rem">
  <p>Visible on screen and on paper.</p>
  <p class="no-print text-muted">Screen only — a hint, a control, a nudge.</p>
  <p class="print-only">Paper only — reference ORD-4417-QK, printed from example.com.</p>
  <div class="keep-together stack stack-1">
    <strong>Signature</strong>
    <p class="text-muted">This block will not be split across two pages.</p>
  </div>
</div>

.print-only is the one to reach for more often than people do. A printed invoice usually wants a reference, a date printed and a "page 1 of 3" that would be noise on screen — and putting them in the markup is simpler than generating a separate print view.

Tokens

Printing does not use Deck's tokens so much as replace them. The :root block inside @media print overrides fifteen of them with literal values — white surfaces, black text, grey lines, no shadows — so every component that reads var(--surface) or var(--text) becomes print-appropriate without any component needing a print rule of its own.

Design tokens this component reads
Token Live Declared value
--bg light-dark(var(--ink-50), var(--ink-1000))
--surface light-dark(var(--ink-0), var(--ink-950))
--surface-2 light-dark(var(--ink-25), var(--ink-900))
--text light-dark(var(--ink-900), var(--ink-100))
--text-muted light-dark(var(--ink-600), var(--ink-400))
--text-faint light-dark(var(--ink-500), var(--ink-500))
--line light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral)))
--line-strong light-dark(var(--ink-300), oklch(36% .018 var(--hue-neutral)))
--shadow-1 0 1px 2px oklch(20% .03 var(--shadow-color) / .06), 0 1px 1px oklch(20% .03 var(--shadow-color) / .04)

The values above are the screen values. On paper each of these becomes a literal: #fff, #000, #333, #555, #bbb, #888 and none. That token-override approach is why so little per-component print CSS is needed.

Accessibility

  • Paper has no assistive technology. A printed page is read by eye or not at all, which makes the ordinary rules sharper rather than looser: anything carried by colour, hover, or an aria-label is gone.
  • Backgrounds are dropped by default, and most readers never turn "background graphics" on. Any status shown only as a coloured fill prints as nothing. This is the single most common way a printed page loses information — see the indicator and timeline pages for what that looks like in practice.
  • Print in black and white before you ship it. Deck's status colours are distinguishable in colour and much less so in greyscale. A "paid" and an "overdue" badge that print as two similar greys are worse than no badge.
  • 10.5pt is small for some readers. It is a sensible default for a dense report, and it is not a sensible default for something an older reader will keep by the phone. Override it per document.
  • Appended URLs are long and unreadable. They use word-break: break-all, which produces addresses broken mid-word across lines. Useful on a reference document, noise on everything else — .no-print-url per link, or suppress them wholesale.
  • Do not hide content from the screen that only exists on paper. .print-only is for restating things — a reference, a date — not for information a screen reader would then never encounter.

Right to left

@page margins are set with the physical margin: 16mm 14mm shorthand, which is symmetrical, so nothing is wrong under dir="rtl". .print-header and .print-footer use inset-inline, so they span correctly in either direction.

The page size is the thing to change: @page { size: letter } is a US default, and most of the world prints A4. That is not an RTL issue but it lands in the same place — a document typeset for letter and printed on A4 loses about 18mm off the bottom.

@page { size: A4; }

One line in your own CSS. There is no cascade layer question here — @page is not subject to layers in the way ordinary rules are, and a later declaration wins.

Reduced motion

Not applicable, and handled anyway: the universal selector inside @media print sets animation: none !important and transition: none !important on everything. Browsers do not run animations when printing regardless, so this is belt and braces — but it also guarantees an element prints in its final state rather than wherever an animation had reached.

Printing this page

The section every other page has is this whole page, so instead: the honest way to check print CSS is your browser's print preview, with background graphics off, at A4 and at letter, in greyscale. Deck's print sheet is good, and it cannot know which of your colours carried meaning.

Chrome's preview updates live as you edit, which makes it a usable development surface rather than a final check.

Overriding it

/* A4 rather than letter */
@page { size: A4; margin: 18mm 16mm; }

@layer app.overrides {
  @media print {
    /* Larger type for a document people keep */
    html, body { font-size: 12pt; }

    /* Drop the appended addresses everywhere */
    a[href^="http"]::after { content: none; }

    /* Start every invoice on its own sheet */
    .invoice + .invoice { break-before: page; }
  }
}

app.overrides is the last layer in Deck's cascade, which is what you want here — the print sheet uses !important liberally, and matching it from a later layer is cleaner than escalating.

When not to use it

  • Not .print-only for anything essential. Screen readers never reach it.
  • Not .no-print as a general hide. It is display: none in a print context only; on screen the element is still there. .hidden or [hidden] is what you want otherwise.
  • Not .print-header for a long header. It is position: fixed and repeats on every page, so anything tall eats the whole document.
  • Not a screen layout for a document people file. If a printed invoice is a real deliverable, it deserves its own template — the print sheet makes a screen page acceptable on paper, not optimal.
  • Not relying on background colour. It is off by default in every browser's print dialogue, and most people never change it.