Technical detail
Protocol, error codes, accessibility and the status behind every promise.
This page is for people who want the detail. Everything here is measured or read off the source — none of it is estimated.
How the widget loads
Two stages. First a small loader that draws the button and does nothing else. The chat itself is fetched only when one of five triggers fires — the browser is idle (after three seconds at the latest), a mouse or finger on the button, a command from the page, a frustration signal from MousePlayer, or a push nudge, for example from your service worker.
There is a reason: most visitors never open the chat. For them it should not hold up the page load; which triggers apply is set on the script tag.
- Loader: 3.48 kB, as our CDN transfers it with gzip
- Whole widget: 85.5 kB, transferred the same way, and only once one of the triggers fires
- Until the button is there: 11–21 ms
- Layout shift: 0.0000 — the space is reserved before anything appears
For comparison, also measured and all three packed with brotli: Crisp's loader is 2,876 B, Intercom's is 3,161 B. At 3,069 B we are in between, not orders of magnitude below. Anyone claiming otherwise has not measured.
It does not disturb your site
The widget lives in a closed shadow DOM. Your styles cannot reach it, its
styles cannot reach your page — not even if you set
* { box-sizing: border-box } or use a CSS framework that redefines
everything.
It runs under a strict Content Security Policy without a single console
message: no eval, no web font, no tracking pixel. It sets its styles through
constructable stylesheets, which style-src does not cover. The button, the
chat window, product tours and the agent console on your site therefore
need no unsafe-inline — measured under style-src 'self' and even under
style-src 'none': zero violations, fully styled. Measured in current
Chromium; older browsers without constructable stylesheets (such as Safari
before 16.4) fall back to a <style> element, which a strict policy blocks.
Zero requests to other servers. Everything comes from reactive.chat or, with your own domain, from your own address; images such as the logo and avatars come from the addresses you provide yourself.
The protocol
The connection uses WebSocket; where a network does not let WebSocket through, it uses plain HTTPS requests (server-sent events or long polling). The protocol is versioned and defines 31 error codes, 26 of them for the widget, the rest for the agent desk and the website search — each with a prescribed client response (retry immediately, retry after a wait, retry after reconnecting, do not retry), some with a close code as well. An error not in the list is treated like a temporary server error: retry after a wait, and after five attempts the message counts as not delivered and can be sent again — no endless loop.
Reconnect with growing backoff. If the connection drops, messages go into a buffer and are sent as soon as it is back. Every message carries an id; the server recognises repeats and does not store them twice.
Accessibility
Built against WCAG 2.2 AA:
- Focus trap in the open window, ESC closes, focus returns to where it was
- New messages are announced via
aria-live - Contrast between text and surface is calculated, not assumed — see below
- Fully keyboard operable, visible focus ring
Contrast is calculated
You give a colour. The surface takes exactly that colour. The text on it is shifted until it reaches a ratio of 4.5:1 — the WCAG requirement for normal text.
Measured across 178 colours in two themes and three pairings, so 1,068 data points: not one below 4.5:1, the worst value 4.50:1.
That is the difference to other providers: there, a widget can be configured until nobody can read it.
Personal data
Masking happens in two places — in the visitor's browser before anything leaves the machine, and again on the server before anything is stored. Recognised are card numbers (with Luhn check), IBANs, e-mail addresses, phone, tax and ID card numbers. Order and customer numbers are left alone.
Two places, because one is not enough: the browser one can be switched off, the server one only sees what arrives.
Rules
When the widget speaks up is defined in a declarative rule list — not in a condition in the code. Six rules ship by default (lingering on a page, rage click, JavaScript error, stuck form, purchase intent, leaving checkout); rage click, stuck form and purchase intent are only detected together with MousePlayer. Your own can be added in your account. On the Free plan, one active prompt per project takes effect.
The difference is practical: changing a rule means changing a setting in your account, not a deployment.
Integrations
Webhooks and Slack are done. For more than 30 further services a connection is ready; most of them are tested against simulated counterparts but have never run against the real service, and the integrations page in your account marks them that way.
The server API (REST and MCP) reads conversations, messages, projects, visitors, events and rules, writes messages and events and registers webhooks. The visitor list and MCP can be switched off per project.
Every integration runs through a queue with a circuit breaker: if a third party hangs, it is skipped for a while after repeated failures. A chat message is never held up by it — that is the most important rule in this layer.
What is not finished
So that nobody finds out while integrating:
- The MousePlayer bridge polls, because MousePlayer does not yet emit the agreed events by itself.
- Attachments, A/B tests and your own domain run as a beta: they work and have been measured, but may still change.
- The e-mail, Telegram, SMS, WhatsApp, Messenger and Instagram channels are tested against simulated counterparts, not yet with real accounts. If a reply through a channel finally fails, the conversation does not currently show it.
More in the documentation
How widget, server and connection work together: Architecture and flow. Connecting, HTTP fallback and reconnecting: Transport. All chapters: documentation.
Aġġornat l-aħħar: