Install
Create a widget in Team settings → Widget (team owners only; every plan includes at least one). Each config gets a public expw_ key, a domain allowlist (submissions are only accepted from pages on domains you list), a mode (feedback, support, or both), and for feedback a target board where reports land.
Then paste the snippet before </head> on your site. It's the GA-style async pattern: a tiny queue stub loads the real script lazily, so it never blocks your page.
<script>
// Exponential feedback widget. Full docs — init options, identify,
// setCustomData, setTheme (dark/light/auto), labels, headless submit:
// https://exponential.at/docs/widget/
(function (w, d, u) {
if (w.ExponentialWidget) return;
var q = [], api = { q: q };
["init","identify","setCustomData","setTheme","open","close","submit"].forEach(function (m) {
api[m] = function () { q.push([m, [].slice.call(arguments)]); };
});
w.ExponentialWidget = api;
var s = d.createElement("script");
s.async = true; s.src = u;
d.head.appendChild(s);
})(window, document, "https://app.exponential.at/widget/v1/loader.js");
ExponentialWidget.init({ key: "expw_YOUR_KEY" });
</script>That's the whole install. A floating feedback button appears, and calls made before the script loads are queued and replayed.
expw_ keys ship in page source, like an analytics ID. The domain allowlist plus server-side rate limits are what gate submissions, so no secret ever has to live in the page.JS API
The snippet exposes window.ExponentialWidget with seven calls:
// Call this once to boot the widget with your public key.
// Optional init overrides: theme ("dark" | "light" | "auto"),
// position, color, label, showButton, zIndex.
ExponentialWidget.init({ key: "expw_YOUR_KEY" });
// Attach your signed-in user, so reports arrive with a
// real reporter (and helpdesk replies reach their inbox).
ExponentialWidget.identify({
email: "ada@example.com",
name: "Ada Lovelace",
userId: "usr_123",
});
// Arbitrary context stamped onto every submission:
// plan, build, feature flags, tenant…
ExponentialWidget.setCustomData({
plan: "business",
version: "1.42.0",
});
// Hook the widget to your site's dark/light toggle — launcher
// and panel restyle live. "auto" follows the visitor's system.
ExponentialWidget.setTheme("light");
// Open / close the panel programmatically. Wire your own
// "Report a bug" menu item to open().
ExponentialWidget.open();
ExponentialWidget.close();
// Submit without the panel. See Headless mode below.
ExponentialWidget.submit({ title: "Broken button" });All calls are safe to make before the script has loaded. The loader queues and replays them in order. (A queued submit runs fire-and-forget; call it after load, e.g. from a click handler, to get its Promise.)
Form fields
The feedback form always asks for a title and details. Everything else is configured per widget in Team settings → Widget:
- Email: shown by default and optional; make it required, or hide it entirely for internal tools where nobody wants resolution emails.
- Name: off by default. Turn it on (optionally required) when a plain name is all you need to walk over and ask “what did you mean?” without collecting an email.
- Custom fields: up to 8 extra text inputs (e.g. “Which page?”, “Order number”). Responses land in the submission's custom-data block, alongside your
setCustomDatapayload. A typed response wins over a host-set key of the same name. - Labels: expose up to 10 of your team's labels (“Bug”, “Idea”, …) as toggle chips, and the reporter's picks arrive on the created issue — triage done at the source.
- Appearance: dark (default), light, or match-the-visitor's-system theme, plus accent, background and text color overrides — all with a live preview in settings.
Visitors attached via identify() skip the email and name fields. Their identity rides along invisibly. Support mode always asks for an email: it's the reply channel.
Headless mode
Want your own feedback UI? Boot the widget without its button and submit programmatically. You keep the key + domain gating, rate limits, and issue creation, and skip the panel entirely:
ExponentialWidget.init({ key: "expw_YOUR_KEY", showButton: false });
ExponentialWidget.identify({ email: "ada@example.com", name: "Ada" });
// Later, from your own form's submit handler:
const result = await ExponentialWidget.submit({
title: "Broken button", // required by the server
description: "Steps to reproduce…",
name: "dani", // overrides identify()
customData: { page: "checkout" }, // merged over setCustomData()
screenshot: myBlob, // optional: you capture it
});
if (result.ok) {
console.log("Filed as", result.identifier); // e.g. "EXP-42"
} else {
console.error(result.error, result.code);
}
// Support mode works too (requires the helpdesk):
await ExponentialWidget.submit({
mode: "support",
message: "I can't log in",
email: "ada@example.com", // required: it's the reply channel
});submit() resolves with { ok, identifier, url } on success and { ok: false, error, code } on failure. It never throws. Screenshots are yours to capture in headless mode; pass a Blob (PNG, JPEG, or WebP) and it's attached like a panel screenshot. Server-side validation (required fields, modes, rate limits) applies exactly as it does to the panel.
Screenshots & annotation
Screenshots are captured client-side, in the browser. The visitor's viewport is rendered locally and nothing is fetched by a server-side browser, so what's on their screen (including logged-in state) is what you see.
On desktop browsers Take screenshot uses the browser's native screen sharing to grab a single frame — it captures content a page snapshot can't render (canvas/WebGL, video, cross-origin iframes). The visitor picks the surface in the browser's own dialog, one frame is taken, and sharing stops immediately. On mobile (and if the visitor dismisses that dialog) the widget falls back to a local page snapshot automatically.
Before submitting, the visitor can annotate the screenshot in a full-screen editor: rectangles, arrows, and freehand lines, with undo. Annotations are flattened into the image on submit.
What lands in Exponential
Each feedback submission becomes, atomically:
- An issue on the configured board, with the visitor's message as the description.
- The screenshot as an attachment, embedded in the issue.
- A metadata block: reporter email (from
identifyor the form), the page URL, browser and viewport details, and yoursetCustomDatapayload.
The reporter is auto-subscribed to the issue. Resolve it and they're notified.
Support requests are different: with the helpdesk enabled (Team plan), they skip the board entirely and open a ticket in your team's Support inbox. That ticket is an email conversation with the reporter that any member can answer, and escalate into an issue on any board when it turns out to be a bug.
Try it
This site runs the real widget. The feedback button in the corner of this page is a live install of exactly the snippet above. Click it, annotate a screenshot, submit, and your report lands on the Exponential team's own feedback board.