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.
| # | Requirement | Surface it governs | What breaks if it is skipped | Where it is specified in full |
|---|---|---|---|---|
| 1 | A 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 sensitive | A 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 |
| 2 | Hidden 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 rendered | A 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. | |
| 3 | Layout 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 value | Revealing reflows the screen, and the presence or absence of a value becomes information in its own right. | |
| 4 | The 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 measured | The number is masked while the category stays legible, so the disclosure the reader asked to prevent happens anyway. | |
| 5 | Content 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 it | The 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. | |
| 6 | Reveal 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 value | Anyone 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. | |
| 7 | Screenshots 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 flow | A block removes a legitimate route for sending a result to family or a clinician, and the reader reaches for a second phone instead. | |
| 8 | Notifications, 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 widgets | Content 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 |
| 9 | Reveal 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 launches | A reader who revealed one number on a train finds every number revealed on the next launch, in whatever room that launch happens in. | |
| 10 | The app is usable with values masked. No reveal is ever required to use the app. | Navigation, data entry and settings, on every screen | Masking becomes a cost the reader pays in function, so they switch it off and it protects nobody. | |
| 11 | The 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 category | The 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.
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.
Consent and disclosure
Consent that is granular, revocable, recorded and re-asked, and the difference between a lawful basis for processing and a person's actual understanding.
Regulatory context
A starting map of the regimes that may apply to a consumer health product, what each one asks of the product, and the large amount opsinjs does not contribute.