KQ-1781 · SAFETY/NUMBERS

Safety Numbers

An end-to-end verification screen that lays both safety numbers side by side as grids of five-digit groups: pointing at or focusing a group lights its partner and draws a hairline between them, and the compare-aloud stepper walks the pairs one at a time with an outline that travels cell to cell on springs.snap. Marking the chat verified turns each matched pair over in 3D on springs.glide, both grids together and staggered in reading order by cascade(), to a calm tick, and once the last one lands a seal settles on the contact on springs.snap with one ring breathing out; a changed number shows its warning first. Each grid is a role="grid" walked with the arrows, Home, End and Ctrl+Home/End, and every group's name spells its digits so a screen reader reads them one by one.

KQ-1781 · SAFETY/NUMBERS

Verify Rafi Okafor

@rafi · Waylight

Not verified

Arrow keys move between groups; each group reads its five digits.

Compare these numbers with Rafi, in person or on a call. If every group matches, your messages are end-to-end encrypted.

Your phone

81496
18583
07650
98345
61074
12321
87616
10165
89812
72323
61898
98587

Rafi's phone

As read
81496
18583
07650
98345
61074
12321
87616
10165
89812
72323
61898
98587

Read the groups to each other on a call, or scan Rafi's code when you're together.

not verified · 12 groups

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/safety-numbers

Props

yourCode
string
default defaultYourCode
The safety number this device computed: digits, any spacing (60 digits make 12 groups).
theirCode
string
default defaultTheirCode
The number on the contact's device, as read or scanned. Groups that differ are struck through and verifying is held back.
contact
SafetyContact
default defaultSafetyContact
Who is being verified: { name, handle?, device? }.
youName
string
default "Your phone"
How this side is named over its grid.
verified / defaultVerified
boolean
default — / false
The verified state, controlled or initial. A change plays the flip and the seal, whoever made it.
onVerify
() => void
default —
"Mark as verified" was pressed and every group matches.
onReset
() => void
default —
"Clear verification" was pressed; the ticks turn back in reverse order.
onScan
() => void
default —
"Scan code" was pressed: open your camera flow, then set verified.
onCompare
(index, result) => void
default —
A group was compared aloud: its index and "same" or "different".
changed / previousCode / onAcknowledgeChange
boolean / string / () => void
default false
The contact's number changed: a warning shows first, and once acknowledged the groups that differ from previousCode carry a dot.
verifiedAt / now
number
default — / defaultSafetyNow
When the chat was verified and the moment relative times count from, in ms since the epoch.
groupSize
number
default 5
Digits per group.
columns
number
default 3
Groups per row in each grid.
groupStyle
"cells" | "chips" | "digits"
default "cells"
Groups as hairline cells, filled pill chips, or one small box per digit.
density
"compact" | "regular" | "roomy"
default "regular"
Digit size, cell height, grid gaps and card padding.
accent
string
default "var(--accent-bright)"
Compare outline, connector, ticks and seal — any CSS colour; fills take its hue at a fixed lightness.
renderCode
(code) => ReactNode
default a seeded picture of the digits
Draws your number as a scannable code in the card's QR view; pass your real encoder here.
status / onRetry
"ready" | "loading" | "error" / () => void
default "ready"
Loading shows skeleton grids; error shows a message and Try again, which calls onRetry.
height
number
default 540
The surface's height in px; the body scrolls inside it.
disabled
boolean
default false
Read only: the numbers show and can be explored, nothing can be marked.
className
string
default —
Classes for the outer section.