All posts

Rebuilding WhatsApp and the Call Screen in CSS

Why we rebuilt WhatsApp chrome and an iOS call screen in CSS rather than screenshotting them, the exact tokens, and why two colour ramps mean something.

Our homepage shows the same agent running on three surfaces: a website widget, WhatsApp, and a phone call. To do that honestly we had to rebuild WhatsApp's chat chrome and an iOS call screen in CSS, down to the hex.

Not screenshot them. Rebuild them.

Why not just take screenshots

Screenshots were the first plan and they failed on three counts.

Text inside a screenshot does not respond. The composition changes across widths, and a captured image either scales until the message text is unreadable or gets cropped until the conversation is gone. The words in these panels are the product demonstration. They cannot be pixels.

A screenshot also welds the homepage to whatever the Studio chrome looked like the day it was captured. A toolbar moves and the marketing site is quietly out of date with no build error to tell you.

And there is a rights problem that gets waved away too often. A WhatsApp screenshot carries WhatsApp's interface, and typically someone's wallpaper. We ship no third-party screenshot and no proprietary wallpaper. The wallpaper in our WhatsApp panel is an original low-contrast asset we drew.

So the channels are rebuilt from interface vocabulary. Recognisable at a glance, ours in every file.

The tokens

Rebuilding a surface means committing to its exact values, which means they belong in the token file next to our own palette:

--color-channel-whatsapp-bg: #0c0f11;
--color-channel-whatsapp-incoming: #1d2529;
--color-channel-whatsapp-outgoing: #005c4b;
--color-channel-whatsapp-accent: #25d366;
--color-channel-whatsapp-tick: #53bdeb;
--color-channel-whatsapp-stamp: rgb(233 237 239 / 60%);

--color-channel-call-accent: #34c759;
--color-channel-call-end: #eb4d3d;

That teal, #005c4b, is the one people recognise without being able to name. The blue tick is #53bdeb. The timestamp on a bubble is not a grey, it is the message text at sixty percent, which is why picking a grey for it always looks slightly wrong.

The call screen runs on the iOS system green and red. Our panel follows the hierarchy of a native call screen and never starts audio.

Getting these approximately right is worse than not doing it. An almost-WhatsApp is uncanny; people cannot say what is off but they know something is. The values have to be exact or the surface should not be imitated at all.

Signal and human

Our own palette has ramps named carbon, slate, mist, canvas, bone, danger, success. Two are named for what they mean rather than what they are.

Signal is the blue. It is the agent.

Human is the orange. It is a person: the handover, the moment a live human takes the conversation.

Naming them that way made a class of bug impossible to write. Blue and orange sit opposite each other, so the two states can never be confused at a glance, in any surface, at any size. And nobody on the team has to remember which blue means what, because the token says which. A reviewer seeing --color-human-400 on an agent message knows immediately that it is wrong.

Colour naming is usually treated as taxonomy. Two of ours are load-bearing.

Three conversations, not one

The panels show three independent conversations with one configuration. They are not one person moving between channels.

That distinction matters because the other reading is a common and mostly fictional demo: a customer starts on the website, continues on WhatsApp, finishes on the phone, context intact throughout. Ours shows the true thing instead, which is the same agent and the same knowledge answering three different people in three different places.

Each channel keeps its own chrome at every width: the browser bar on Website, the contact bar on WhatsApp, the call header on Phone. Take those away to save space and you have three chat boxes with different colour schemes, which demonstrates nothing.

Messages across all three arrive on a shared cadence, each growing from zero height and pushing the history above it upward. The shared timing is what makes the three panels read as one demonstration rather than three animations that happen to be adjacent.

The control, and what it is not allowed to cover

A narrow persistent rail is the only channel control. Nothing floats over the surfaces and nothing shrinks them.

On phone widths the rail moves to the bottom and its space is reserved inside a compact portrait frame, so it never covers the conversation. The website widget docks across the full width above the control rather than collapsing, so at every width, on every channel, the conversation stays visible.

The theatre never reacts to scroll and never takes control of it. Every state is directly selectable, the whole timed sequence can be replayed explicitly, and reduced motion shows the same states with no transitions. It is a demonstration, not a ride.

The general version

If your product lives inside someone else's surface, your marketing has to render that surface, and there are only two honest ways to do it: capture it with permission, or rebuild it exactly.

Rebuilding costs a week and a token file. What it buys is a demonstration that stays legible at every width, never drifts from the shipped product, and does not ship a single pixel that belongs to somebody else.