opsinjs
HealthConsent and privacy

On-screen privacy

Health data is read on trains, in offices and over shoulders. The page sets how much of it an interface exposes to the room around the reader.

What this means

Privacy engineering in health products concentrates on what is invisible: encryption, access control, retention periods and sharing agreements. All of it is necessary, and none of it governs the disclosure this page is written against, which happens when a reader opens the app on a train with someone sitting next to them.

Everything rendered on a screen is visible to anyone with line of sight to the device, and three common layout conventions widen that exposure rather than narrowing it: the most sensitive number is rendered at the largest size, at the highest contrast, and in the position the eye reaches first. A condition name in a section header is legible from a metre away, and an app icon carrying a badge on a lock screen discloses that the app has something waiting, without the app being opened at all.

A second exposure sits on a surface the app does not draw. When someone backgrounds an app, the operating system captures a snapshot of the current screen and shows it in the multitasking view, where it persists and where other people looking at the device will see it.

The rule

Sensitive values are hidden by default where the reader has asked for that, the app never leaks its content through system surfaces, and re-authentication is required to reveal.

Every requirement below is the product's to implement, because no opsinjs component carries a masking, hiding or sensitivity prop today. "Components that implement this" states that gap in full.

#RequirementSurface it governsWhat breaks if it is skippedWhere it is specified in full
1A per-user privacy setting that is easy to find. The setting is reachable without a search through nested preferences, the private behaviour is the default rather than something the reader switches on, and categories the reader has marked sensitive start at the more private option.The settings surface, and every screen rendering a category marked sensitiveA reader who wants masking never finds the control, and a default of visible protects only the readers who already knew to look for it.Health data's standing as a special category under the UK and EU GDPR is set out in Consent and disclosure
2Hidden values are masked rather than obscured. The value is replaced by a fixed-width mask, never by a blurred, shortened or otherwise distorted rendering of the real thing.The value itself, at every size it is renderedA blurred number can often be read and a shortened one can be inferred from its length, so the value is disclosed by a treatment that looks protective.
3Layout does not shift on reveal. A masked value occupies the same space as the real one.The layout of the card, row or tile holding the valueRevealing reflows the screen, and the presence or absence of a value becomes information in its own right.
4The label is hidden where the label is the disclosure. For sensitive categories the section heading is often more revealing than the number, and it is hidden alongside the value.Section headings, category names and any other text naming what is measuredThe number is masked while the category stays legible, so the disclosure the reader asked to prevent happens anyway.
5Content is blurred or replaced on backgrounding. The cover is applied before the system snapshot is taken and removed when the app returns to the foreground.The app switcher, and the system snapshot behind itThe snapshot holds a readable screen for as long as the card persists in the multitasking view, in front of anyone who picks the device up.
6Reveal requires re-authentication. Revealing a hidden value uses the platform's biometric or passcode flow with a timeout short enough to matter, and a non-biometric route is always offered, because biometrics fail and not everyone can use them.The reveal control on every masked valueAnyone holding the unlocked phone can reveal the values, and a biometric-only route locks out readers whose biometrics fail or who cannot use them at all.
7Screenshots are treated as a user right. Capture is never blocked, a warning is shown once when a sensitive screen is captured, and an intentional redacted share is the easier of the two paths.The platform capture action, and the product's own share flowA block removes a legitimate route for sending a result to family or a clinician, and the reader reaches for a second phone instead.
8Notifications, badges and widgets are on-screen privacy surfaces. They carry health content to surfaces the app does not control and are governed by the same reasoning as the screen.The lock screen, the notification shade, the app icon badge and home-screen widgetsContent masked inside the app is published in full to a lock screen, which displays notification content without authentication by default.Notifications and off-screen alerts
9Reveal is per value and per session. A reveal applies to the value revealed and expires with the session rather than persisting by default.The reveal state held across values and across launchesA reader who revealed one number on a train finds every number revealed on the next launch, in whatever room that launch happens in.
10The app is usable with values masked. No reveal is ever required to use the app.Navigation, data entry and settings, on every screenMasking becomes a cost the reader pays in function, so they switch it off and it protects nobody.
11The shoulder-surfing case is checked in review. The review asks what is legible at a metre and at an angle.The design review, for every screen showing a sensitive categoryThe legibility of a screen to a second person is never tested, so the ten requirements above hold only where someone happened to think of them.

The last column is filled only where another page carries the requirement, or the material it depends on, in full. Rules 2 to 7 and 9 to 11 have no such destination and their cells are left empty rather than pointed at a page that does not cover them. Rules 2, 3 and 4 have no component prop that implements them today, which the note at the end of this page records.

Why (evidence)

Applying it

Do

Render a masked value as a fixed-width block of the same size and weight as the real value, with an explicit reveal control that has an accessible name: "Show your result".

Don’t

Apply a CSS blur to the number. A blur is often legible at a glance to someone sitting beside the reader, it is a lossy transform that still carries the real value to anyone who captures it, and it is trivially removed from a screenshot.

Do

Cover the whole window with an opaque screen on resign active, before the snapshot, and remove it on return.

Don’t

Leave the results screen live in the app switcher. The snapshot is then visible to anyone who picks up the phone, for as long as the card persists in the multitasking view.

Do

Collapse "Sexual health" to "Results", with the category name hidden, when the reader has marked that category sensitive.

Don’t

Mask the number and leave the heading. A person beside the device reads the category from the heading, which for a sensitive category discloses more than the value does.

Do

Offer "Share a summary", which produces a composed, redactable document whose contents the reader chooses.

Don’t

Block screenshots. The reader photographs the screen with another phone, and now there is an unencrypted image with no redaction in someone's camera roll.

Components that implement this

Value is specified to support a masked state that reserves identical layout, because rules 2 and 3 are only reliable if the masking happens at the same place the formatting does. Surfaces that carry sensitive categories are specified to take a sensitivity flag rather than each screen inventing its own treatment. Both of those remain specifications rather than shipped behaviour. Nothing that ships today has a masking, hiding or sensitivity prop of any kind. MetricTile says so in its own file, and the others have no such affordance either. Every rule on this page is therefore the product's to keep in its own code, and a screen that keeps none of them will look exactly like a screen that keeps all of them.

What this does not cover

  • Authentication, session management and device security, which are your application's concern.
  • Encryption at rest or in transit, and everything else that happens off the screen.
  • Screen recording and remote access, including accessibility services with broad permissions on the device.
  • Shared devices and household use, where the threat model changes substantially and account separation matters more than masking.
  • Physical privacy filters, which are a hardware answer to the same problem.
  • What may appear in a notification, which is Notifications and off-screen alerts.
Not written yet. Specify the masked state on Value and the sensitivity flag as real props, including what a mask renders as at every size and what the reveal control is called for a screen reader. Until they exist, rules 2, 3 and 4 cannot be met by using an opsinjs component and have to be built again on each screen.

Updates to this page

Last read through against the system on 2026-09-02. Due for review every 6 months; expiry is reported by pnpm run check:freshness.

On this page