The Tapko widget has one job: let your users submit feedback without slowing down your app. That sounds simple, but it put three hard constraints on the build. First, the widget cannot block page rendering — not even for a millisecond. Second, it must work on any website, regardless of framework. Third, it has to stay small. Every kilobyte you add to a third-party script is a kilobyte your users pay for on every page load.
Why Vanilla JS, Not a Framework
The first question we answered was the framework question. React, Vue, and Svelte are all fine choices for apps you control. They're poor choices for an embeddable CDN script. A React widget would bundle the entire React runtime — even if your host page already uses React. A Svelte widget would be smaller, but still brings a compiler and a component model that's unnecessary for a text field and a button.
Vanilla JS gave us complete control over what ships. No runtime, no virtual DOM, no reconciler. Just the DOM APIs that every browser already has. The final widget bundle is 7.4KB gzipped.
Async Loading Without Race Conditions
The widget loads via a single script tag with async and defer. But async loading introduces a problem: your page might try to initialize Tapko before the script has finished loading. We solve this with a command queue pattern — the same approach Google Analytics has used for years.
// Snippet that goes in your <head> — runs immediately, before widget loads
window.tapko = window.tapko || [];
window.tapko.push(['init', { projectId: 'YOUR_PROJECT_ID' }]);
// Inside widget.js — drains the queue when it loads
(function () {
const queue = window.tapko || [];
window.tapko = {
push: (cmd) => handleCommand(cmd),
};
queue.forEach(handleCommand);
function handleCommand([action, options]) {
if (action === 'init') mountWidget(options);
}
})();Commands pushed before the widget loads are buffered in the array. The moment the script runs, it replaces the array with a live object and replays every buffered command. Your page never needs to wait for the widget to be ready.
“An embeddable widget is a guest in someone else's house. It should be invisible until needed, and leave no trace when it's gone.”
Shadow DOM for Style Isolation
The widget renders inside a Shadow DOM boundary. This means your host page's CSS cannot leak into the widget, and the widget's styles cannot leak out and break your page. The widget always looks exactly as designed, regardless of what CSS framework or reset the host page uses.
It also means uninstalling is clean. Remove the script tag, and nothing the widget created persists in the DOM or the global scope.
7.4KB Gzipped
No framework dependencies. The full widget fits in less than a typical image thumbnail.
Shadow DOM Isolated
Widget styles never conflict with your page — and your page styles never break the widget.