---
title: "Print and export"
description: "Compliance reviewers read on paper and patients print results for appointments. What the print stylesheet does, and what a printed health value must carry."
url: "https://opsinjs.pensievelabs.org/accessibility/print-and-export"
source: "https://opsinjs.pensievelabs.org/accessibility/print-and-export.md"
section: "Accessibility"
kind: "accessibility"
reviewed: "2026-09-02"
reviewer: "design"
aliases: ["print stylesheet", "pdf", "paper", "photocopy"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<PageTemplate kind="accessibility" />

## What we guarantee [#what-we-guarantee]

Printing is treated here as an accessibility surface rather than a convenience,
for two reasons that are specific to this domain. Compliance reviewers read
documentation on paper and annotate it. And patients print or screenshot results
to take to an appointment, which means a printed health value becomes a clinical
artefact in somebody else's hands, stripped of every tooltip and hover state that
explained it.

`app/globals.css` carries a real `@media print` block. It does the following, and
each item is there because of a specific failure:

* **Removes chrome that carries no information on paper,** so the printed page
  starts with the content. The banner, the main navigation, the sidebar, the
  sub-navigation and the table of contents are each named individually. It never
  removes `aside` as an element, and it says so in a comment: `<StubNotice>`,
  `<SafetyCallout>` and `<ResearchNote>` are all asides, and the same block
  restores those three positively so that a later chrome selector cannot quietly
  take them out again. A caveat that vanishes on paper is worse than no caveat,
  because the reader cannot tell it was ever there.
* **Forces a white background and black text at 11pt,** because a dark-theme
  print is unreadable and wastes toner.
* **Strips `backdrop-filter`, `box-shadow` and `text-shadow` globally.**
  Translucency does not survive a printer; it prints as a grey smear over the
  text.
* **Puts a `1pt` solid black border around every `[data-status]` element** and
  sets `print-color-adjust: exact`. The status word is always present, so the
  colour is not load-bearing. Even so, the border means the pill is still
  visibly a discrete object after a black-and-white photocopy.
* **Resolves external link targets into the text,** appending the URL after any
  `http` link inside `main`. A printed page with a bare "see here" is useless in
  a review file.
* **Prevents tables, figures, code blocks, block quotes and safety callouts from
  splitting across a page break,** and keeps headings with the content that
  follows them. A threshold table broken across two sheets is a classic way for a
  reviewer to misread a number.
* **Sets an 18 mm page margin,** so there is room to write in.

The product theme adds a second `@media print` block of its own in
`app/product.css`. It redeclares `--background`, `--card`, `--foreground`,
`--card-foreground`, `--muted-foreground` and `--border` as ink on paper, and it
carries every material rung to its opaque fallback. So a screen printed from the
dark theme prints as ink on white rather than as pale text on a dark fill. This is
the half a consuming product actually ships, because the product stylesheet travels
with the theme a product installs while the docs-site `globals.css` above does not.

## What you own [#what-you-own]

**What a printed result must still say.** This is the important half of the page.
A health value on paper has lost its context and gained an unknown reader. It
must still carry:

1. **The value, its unit and its precision.** They are never truncated and never
   abbreviated past recognition.
2. **When it was measured,** as an absolute date and time. `RelativeTime` renders
   "2 hours ago" on screen, which is meaningless on a sheet of paper found next
   week. Anything printed resolves to an absolute stamp.
3. **What measured it,** where that affects how the number should be read. See
   [Data provenance and device accuracy](../health/data-provenance-and-device-accuracy.mdx).
4. **The status word,** not the status colour.
5. **The disclaimer,** if one was shown on screen. A printout that drops the "this
   is not a diagnosis" line has changed what the document claims.
6. **A visible boundary.** The print stylesheet drops a background colour and a
   box-shadow, and it keeps an outline and a border. A surface that leaned on its
   fill or its shadow to show where it ends prints as boundaryless content on a
   same-coloured page. opsinjs draws a [Surface](../components/surface.mdx) edge as
   an outline for exactly this reason, so a surface at a rung that publishes a
   border keeps its boundary on paper. That is the `card`, `raised`, `sheet` and
   `overlay` rungs, which is what Card, MetricTile, Dialog and Sheet render. The
   three rungs that publish no border by design, `canvas`, `inset` and `scrim`,
   draw no edge on screen or on paper, so a panel built on one of those prints
   boundaryless, and a product that draws its own container edge must reach for a
   property in the second group, an outline or a real border, never the first.

**Pagination of long data.** A twelve-month log printed as one enormous table
needs repeated headers on each page. That is a product decision.

**Whether printing is offered at all.** Some data should not be trivially
exported. See [On-screen privacy](../health/on-screen-privacy.mdx).

<SafetyCallout severity="attention" evidence="opinion">
  A printed page cannot be updated. If a value is provisional, unconfirmed or from
  an unvalidated source, say so *in the printed output*, not only in the interface
  that produced it. This is our opinion and it is the rule we would defend hardest
  on this page.
</SafetyCallout>

## How to check [#how-to-check]

<Steps>
  ### Print to PDF, then read the PDF [#print-to-pdf-then-read-the-pdf]

  Read the file rather than the print preview. &#x2A;*Expected result:** no navigation,
  no translucency, no split tables, every external link resolved, and a date on
  every value. **Failure looks like** the sidebar occupying the first two pages.

  ### Photocopy it in black and white [#photocopy-it-in-black-and-white]

  Or print it greyscale. &#x2A;*Expected result:** every status is still identifiable
  from its word and its border. This is the same check as
  [Colour independence](./colour-independence.mdx), applied to the medium where
  colour is most likely to be lost.

  ### Print at a different paper size [#print-at-a-different-paper-size]

  A4 and US Letter differ enough to break a fixed-width table. Check both if the
  audience is international.

  ### Hand it to somebody with no context [#hand-it-to-somebody-with-no-context]

  Give the printed page to a colleague who did not see the screen and ask what the
  numbers mean and when they were taken. Anything they cannot answer is missing
  from the output.
</Steps>

## Measured results [#measured-results]

<NoDataYet script="scripts/capture-registry.mts" />

There is no automated print check. A rendered-to-PDF snapshot of the reviewer
path would be the right artefact, and it needs a browser automation pass that
this project deliberately does not install.

## Known gaps [#known-gaps]

* **The print stylesheet has never been tested against a real printer,** only
  reasoned about.
* **There is no PDF export path,** for the reviewer bundle or for anything else;
  printing is the only route to paper.
* **The printed-value rules are implemented per component, but nothing verifies a
  whole screen end to end.** RelativeTime resolves to an absolute stamp in print
  (`print:not-sr-only`), Surface draws a printed boundary on every rung that
  publishes a border, and several components ship `print:` rules, so the rules are
  no longer absent. What
  is still missing is any check that one screen carries the value with its unit and
  precision, the status word and the disclaimer onto paper together, because there
  is no automated print capture to assert against.
* **Page numbers, running headers and a printed provenance footer are not
  implemented,** beyond the review stamp the stylesheet appends to
  `[data-reviewed]`.

## Updates to this page [#updates-to-this-page]

<Reviewed />
