Back to all posts

Building an embeddable chat widget in three lines of code.

Three lines is the easy part to promise and the hard part to actually ship without breaking someone else's website. Here's what those three lines have to do.

KJ
Karan Joshi
Design

"Three lines of code" is an easy line to put on a marketing page and a genuinely hard constraint to design against. Every business that installs AIVA's web widget is dropping our code into a site we've never seen, built on a stack we don't control — WordPress, Webflow, a hand-rolled React app, a Shopify theme with six plugins already fighting each other. The widget has to work identically on all of them, without the business touching anything beyond pasting a snippet once.

What the three lines actually have to carry

The visible part is small on purpose:

<script src="https://widget.aivachat.io/embed.js" async></script>
<script>window.AIVA_SITE_ID = "your-site-id"</script>
<script>Aiva.init()</script>

Everything else — how the widget looks, when it shows up, what it's branded as, which pages it avoids — is configuration pulled at runtime from that site ID, not code the business has to write or redeploy to change. That split is the actual design decision: the snippet's job is only to say "this page belongs to this business." Behavior lives on our side, so a business can change their greeting or their trigger rules from a dashboard without ever touching their site again.

In practice this means the loader script does very little on its own. It fetches a small configuration payload keyed to the site ID — branding, trigger conditions, which pages to avoid — and only then constructs the actual widget interface. A business changing their widget color in the dashboard doesn't touch the embed snippet at all; the next visitor simply gets a fresh config fetch with the new value already in it. That's what makes the three lines durable. They almost never need to be touched again after the first paste, because nothing that changes over a widget's lifetime lives in those three lines.

The constraints nobody sees on the marketing page

Several things have to be true simultaneously, and they pull against each other.

It can't slow the host page down. The script loads async, so it never blocks the page's own rendering. If AIVA's servers are slow to respond, the worst case is the widget appears a beat late — never a host site that hangs waiting on us.

Nothing leaks either direction. A host site's CSS resets, its font declarations, its * { box-sizing: border-box } — none of it should touch the widget's layout. And the widget's own styles can't leak out and change how the host page renders. We isolate the widget's DOM so the two style systems never see each other, which is what makes "works on literally any site" actually true instead of "works on the sites we tested." In practice that means treating the widget as a sealed unit with its own style scope, the same general approach browsers themselves use to keep things like native form controls from inheriting a page's styles by accident — the widget brings its own rules and ignores the host's.

It has to survive being installed wrong. Businesses paste snippets into places designed for other things — a tag manager container, a theme's custom-HTML widget, a page builder's embed block, sometimes twice by accident because someone forgot it was already there. The script has to no-op cleanly on a duplicate load rather than mounting two widgets, and keep working inside whatever wrapper a page builder puts around it.

It has to fail quietly, not loudly. If our servers are unreachable, or the site ID is misconfigured, the correct behavior is for the widget to simply not appear — not to throw a visible error, not to leave a broken placeholder in the corner of someone's homepage. A business's site should never look broken because of us, even in a genuine outage.

The hardest bugs we've shipped on the widget were never about the widget itself — they were about some other script on the host page doing something unexpected, three years ago, that nobody at the business remembers is there. Defensive code against an unknown host environment is most of the actual engineering here.

The environment problems that don't show up in a demo

A widget that works perfectly in our own test environment still has to survive the actual diversity of the open web. Ad blockers and privacy extensions routinely block third-party scripts by pattern-matching domain names and script behavior, so the loader has to degrade gracefully rather than leave half-initialized state behind when it gets blocked partway through. Some host sites run a strict Content Security Policy that whitelists exactly which external scripts are allowed to run — when that happens, the fix isn't on our end at all, it's a one-line addition the business's developer makes to their own CSP header, and we document that case specifically because it's the one installation failure that isn't something we can route around.

Single-page apps add a different wrinkle. On a traditional site, a new page load means the widget's script runs fresh every time. On a React or Vue app that changes "pages" without a real browser navigation, the script only runs once, ever — so trigger logic tied to "which page is this" has to watch for client-side route changes explicitly, rather than assuming a fresh page load will re-evaluate anything. Get this wrong and the widget either never updates its behavior after the first route, or re-initializes itself repeatedly and leaks duplicate instances.

Mobile adds its own set of constraints on top. A widget bubble sized comfortably for a desktop corner can crowd a small screen or sit awkwardly close to an OS-level gesture area if it isn't built with a phone's viewport in mind from the start, and an open conversation window has to behave like the rest of a mobile web page — resizing correctly when the on-screen keyboard appears, not trapping scroll on the page underneath it. None of this is exotic engineering, but skipping it is exactly how a widget that looks polished on a laptop ends up covering a business's own phone number on a visitor's iPhone.

Keeping configuration current without a redeploy

Because branding and trigger rules live in configuration rather than in the snippet itself, the loader has to balance two things that pull in opposite directions: fetching a fresh configuration on every page view would be wasteful and slow for a setting that rarely changes, but caching it too aggressively means a business that just updated their greeting in the dashboard doesn't see the change reflected for visitors already holding a stale copy. The practical answer is a short-lived cache with a sensible expiry, not an indefinite one — recent enough that a business sees their own change within a reasonable window, cached long enough that a visitor browsing five pages in a row doesn't trigger five separate configuration fetches for a widget that hasn't changed at all.

Why this is worth getting right

The alternative to a script-tag embed is an SDK — a package to install, a build step to configure, something that needs a developer involved every time it changes. That's a fine model for a developer-facing tool. It's the wrong one for a clinic or salon owner who wants the widget live today and wants to change its greeting themselves next month without filing a request. We've written separately about what it actually looks like to add the widget without hiring a developer — the three-line constraint above is the engineering reason that's even possible.

Three lines, pasted once, is the whole point: everything a business will ever want to adjust about how AIVA shows up on their site — when it appears, how it's branded, what it says first — should be a setting, not a redeploy. The widget is also just one adapter into the same conversation engine that runs voice and SMS, which is worth remembering when the three lines feel almost too simple for what shows up once someone actually starts typing. You can see the finished result on the web widget platform page, or check the integrations we already support if the widget needs to talk to a booking system beyond AIVA's own.

Share
KJ
Written by
Karan Joshi
Design

FAQ

Common questions.

Paste a three-line script snippet into your site once — it works the same way on WordPress, Webflow, Shopify, or a custom-built site. Everything else, like appearance and behavior, is configured afterward from a dashboard, not in code.

The script loads asynchronously, so it never blocks your page's own rendering. If AIVA's servers respond slowly, the worst case is the widget appearing a moment late — your site never waits on it.

No. The widget's DOM is isolated from the host page, so a site's CSS resets and font rules don't touch the widget's layout, and the widget's own styles can't leak out and affect the rest of the page.

The script is built to no-op on a duplicate load rather than mounting two widgets, which matters because businesses sometimes install it through a tag manager and a page builder at the same time without realizing it.

No. The three-line snippet is the only code involved. Greeting text, colors, position, and trigger rules are all settings in a dashboard, changeable anytime without touching your site again.

Yes — that's specifically what it's built to survive. The script is designed to keep working inside whatever wrapper a page builder puts around it and to no-op cleanly rather than conflict with other scripts already on the page.

An SDK needs a developer involved every time something changes, which is the wrong model for a clinic or salon owner who wants the widget live today and wants to update it themselves next month.

Like this? Get more.

One email a month. Engineering deep-dives, product launches, customer stories. No fluff.

4,200+ subscribers. Unsubscribe anytime.