Watermark
FreeA repeating overlay tiled from the ROTATED bounding box, so the marks neither collide nor gap at an angle. Staggered rows, an opacity band that says which way it is wrong, and a signature a host can compare against to detect tampering.
import { SvWatermarkComponent } from '@styloviz/watermark';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
One centred mark
<sv-watermark variant="single" text="DRAFT" [angle]="-20" [fontSize]="48" [opacity]="0.12">…</sv-watermark>
Measure manually
Re-tiling on resize and after web fonts load is automatic. Turn it off to drive measure() yourself.
<sv-watermark #mark text="Confidential" [observeResize]="false">…</sv-watermark> // after the container changes size mark.measure();
A viewer token instead of a name
The id is hashed into an opaque token; the raw id never reaches the DOM. Salt it with a value the client cannot guess, or mint the token on the server.
<sv-watermark text="Confidential" [viewerId]="user.id" [viewerSalt]="deploymentSalt">…</sv-watermark> // or build the text yourself import { svViewerToken } from '@styloviz/watermark'; const text = 'Confidential · ' + svViewerToken(user.id, deploymentSalt);
Detect a removed or edited mark
Capture the signature once, then compare periodically. It checks the configuration, the rendered overlay (present, visible, untransformed, at the configured opacity, carrying the signature) and every mark in it (the expected count, the expected text, none hidden, transparent or zero-size), so an edit in place is caught as well as a deletion. It proves a DOM change in this browser — nothing more.
<sv-watermark #mark text="Confidential" (tampered)="onTampered()">…</sv-watermark> const expected = mark.signature(); setInterval(() => mark.checkIntegrity(expected), 2000);
When to use
- Marking a document, preview or export with its classification or the viewer's identity.
- Deterring the casual crop-and-share, which is what almost every real leak actually is.
- NOT as a security control — anything rendered in the browser can be removed by somebody with the page source.
Async states
None — this component has nothing to load, so it has no loading, empty or error state to drive.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| variant | WatermarkVariant | 'tiled' | Presentation: `'tiled'` across the whole box, one centred `'single'` mark, or `'inspect'` (tiled plus a note on what the mark is and is not). |
| text | string | 'Confidential' | The mark. ⚠️ PRIVACY: whatever you put here is published to every screenshot, screen share and support ticket the page appears in — and to the DOM, the visually-hidden label and the `data-sv-signature` attribute. Do not put a viewer's name or email here; bind `viewerId` (or a server-minted token) instead. Dev mode warns once when the text looks like an email address. |
| viewerId | string | number | null | null | An id identifying the viewer, hashed into an opaque token by `svViewerToken(viewerId, viewerSalt)` and appended to the mark as `text · token`. The raw id never reaches the DOM. THE TOKEN IDENTIFIES THE VIEWER ONLY TO WHOEVER CAN MAP IT BACK — your server — rather than to every recipient of every screenshot. It is a hash, not a secret: see `svViewerToken` for why a guessable id space needs a salt. |
| viewerSalt | string | '' | Salt mixed into the viewer token. Use a per-deployment value the client cannot guess. |
| angle | number | -30 | Rotation in degrees. |
| opacity | number | 0.08 | 0–1. Outside roughly 0.04–0.25 the mark stops working — see `opacityVerdict`. |
| fontSize | number | 14 | Font size in px. |
| gapX | number | 80 | Extra space between marks, in px. |
| gapY | number | 60 | Extra vertical space between rows of marks, in px. |
| stagger | boolean | true | Offset every other row by half a tile. A square grid reads as columns, and columns leave clean vertical stripes a screenshot can be cropped to. |
| observeResize | boolean | true | Re-tile automatically when the box resizes (a `ResizeObserver` through `svElementSize`, throttled to one measurement per frame) and when web fonts finish loading. Turn it off to drive `measure()` yourself — e.g. for a box whose size you already track, or in a test. WITHOUT IT A RESIZED BOX KEEPS THE TILING IT WAS MEASURED AT — gaps down one edge when it grows, marks stacked past the clip when it shrinks. The observer measures the border box on the first render, so the first paint is already tiled; the server renders no marks (zero size) and the browser tiles on hydration. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| tampered | void | Emitted by `checkIntegrity()` when the mark in the DOM no longer matches the signature the host captured. WHAT IT PROVES, PRECISELY: that at the moment of the check, in this browser, the rendered overlay was missing, hidden, or carried a different configuration (text, angle, opacity, font size) than the one expected — i.e. a client-side DOM mutation, or a configuration change, was observed. ⚠️ WHAT IT DOES NOT PROVE: that the content is protected, that nobody screenshotted it first, that a silent check means the mark was intact (the check runs in the same page an attacker controls; they can stub it, block the event, or copy the content out of the DOM without touching the overlay), or anything a compliance control needs — it is not an audit record, not tamper-evident, and not server-verified. The mark is not restored. Watermarking is deterrence, not security: treat this event as a telemetry hint, never as a control. A durable mark is burned in on the server (an exported PDF or image). |
Customization & theming
- Three variants: `tiled`, `single` and `inspect` (which shows the tile arithmetic and the opacity verdict).
- Any `text`, `angle`, `fontSize`, `opacity` and per-axis gaps.
- Row stagger on or off.
- `signature()` + `checkIntegrity(expected)` + the `tampered` output, which reports (it does not restore) a removed, hidden or edited overlay.
- Threat model: watermarking is deterrence, not security. `tampered` means a client-side DOM mutation (or configuration change) was observed; it does not mean the content is protected, and it is not a compliance control.
- Privacy: `viewerId` + `viewerSalt` append an opaque token (`svViewerToken`, a 32-bit FNV-1a hash — not a secret; salt it or mint it server-side). The mark text also lands in the DOM, the visually-hidden label and `data-sv-signature`.
- Measurement: re-tiling is automatic — a ResizeObserver (`svElementSize`, border box, one measurement per frame) re-tiles when the box resizes, and `document.fonts` re-measures the text after web fonts load. Set `[observeResize]="false"` to call `measure()` yourself. The server renders no marks (zero size); the browser tiles on hydration.
- Variant axes: `variant` (tiled, single, inspect) and `angle`, `fontSize`, `opacity`, gaps, `stagger` are appearance; `observeResize` and `viewerId` are behavioural and combine with every variant. `single` ignores gaps and stagger.
- Containing block: the overlay is `position: absolute; inset: 0` inside the component's own relative box, so it covers what is projected into it — not a scrolled-away part of an inner scroll container, and not a body portal (menus, dialogs) opened from inside it.
- Browser limits: `document.fonts` is missing in some embedded webviews (the estimate is kept); forced-colors mode may repaint the marks in system colours; and anything rendered client-side can be removed by somebody with the page source.
Best practices
- Do: Keep the opacity between about 0.04 and 0.25 — the `inspect` variant tells you which side you are on and why.
- Do: Identify the viewer with an opaque token (`viewerId`, or `svViewerToken()`), not a name or email — the mark is published to every screenshot, screen share and support ticket the page appears in. Dev mode warns once when the text looks like an email address.
- Don't: Don't treat `tampered` as a control. It proves only that the overlay was changed in this browser at the time of the check; it proves nothing about screenshots taken before, it is not an audit record, and silence proves nothing — the check runs inside the page an attacker controls. A durable mark is burned in on the server (an exported PDF or image).
- Tip: Leave `stagger` on: a square grid reads as columns, and columns leave clean vertical stripes a screenshot can be cropped to.
- Don't: Don't present it as protection. Say what it is — a deterrent — or the belief that it protects will stop somebody doing something that works.
Accessibility
- The overlay is `pointer-events: none`, so it never intercepts a click meant for the content underneath — the commonest way this control ships broken, and one whose symptom (buttons that do nothing) never looks like a watermark bug.
- The repeated marks are `aria-hidden`; the mark is announced once, in words, in a visually-hidden label. A screen reader reading "Confidential" forty times is not accessibility.
- The marks are `select-none`, so a watermark never lands in the clipboard when somebody copies a paragraph.
Feedback on Watermark
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.