Widget
Install the feedback widget, understand when it shows, and control it from your app.
The widget is the part your clients see: a floating button that lets them draw a rectangle on the page, type a comment, and send it — pinned to the exact element.
npm install github:gnoopy/instafix#widget-distimport { initInstaFix } from "@instafix/widget";
const instafix = initInstaFix({
endpoint: "/api/instafix",
projectName: "my-project",
});initInstaFix is the package's main runtime export, alongside the two i18n helpers (registerLocale, loadLocale) and the TypeScript types. It weighs about 30 KB gzipped (ESM), lazy-loads its heavier parts (panel, locales, screenshot engine), and renders inside a closed Shadow DOM so your page styles and the widget's never collide.
When the widget shows — and when it doesn't
initInstaFix runs a series of guards, in this order. When one skips, you get a no-op instance back (every method exists, nothing renders):
- Server-side rendering — no
window? The widget skips withonSkip("ssr"). Never bypassed. - Already initialized — a second
initInstaFix()call returns the existing instance (one widget per page).destroy()resets this. - Production — when
process.env.NODE_ENV === "production", the widget skips withonSkip("production"). This is the "clients see it during review, visitors never do" default. Bypass withforceShow: truefor staging/preview environments. Note: onlyprocess.env.NODE_ENVis read — noimport.meta.env. - Small viewports — below
minViewportWidth(default 768 px) it skips withonSkip("mobile"). Also bypassed byforceShow. - Config validation — no
endpoint/store, or noprojectName? The widget logs aconsole.errorand no-ops. This one does not callonSkip— check the console, not the callback.
The instance
const instafix = initInstaFix({ ... });
instafix.open(); // open the feedback panel
instafix.close(); // close it
instafix.refresh(); // re-fetch feedbacks for the current page (never throws)
instafix.focusFeedback(id); // scroll to + highlight a marker; false if unknown
const off = instafix.on("feedback:sent", (feedback) => { ... });
instafix.destroy(); // full teardown, restores all patched globalson() returns an unsubscribe function. Seven public events are available:
| Event | Payload | Fires when |
|---|---|---|
feedback:sent | the created feedback | A submission succeeded |
feedback:deleted | the feedback id | A feedback was deleted |
feedback:error | the Error | A feedback API call failed — same payload as the onError config callback |
panel:open / panel:close | — | The panel opened / closed |
annotation:start / annotation:end | — | An annotation session started / ended. end fires whether the user submitted or cancelled, and both entry paths — the FAB's draw flow and the toolbar's auto-target picker — emit the symmetric pair, so you can pause chat bubbles or analytics overlays in between |
React
Use the dedicated hook — it survives StrictMode double-mounts and lets callbacks change between renders without re-initializing:
"use client";
import { useInstaFix } from "@instafix/widget/react";
export function Feedback() {
const instafix = useInstaFix({
endpoint: "/api/instafix",
projectName: "my-project",
onFeedbackSent: (f) => console.log("new feedback", f.id),
});
return null; // the widget renders itself
}The hook returns null until it mounts, then the instance. Changing endpoint or another structural option requires a remount; changing a callback does not — every callback the config accepts (onFeedbackSent, onError, onOpen, onClose, onAnnotationStart, onAnnotationEnd, onSkip) is read at call time, so a handler that closes over fresh state fires with that state. They also go quiet after unmount, so a late response can't call into a torn-down tree.
Handling errors
onError receives errors with two useful fields — code ("NETWORK" | "VALIDATION" | "AUTH" | "SERVER") and retryable (boolean):
initInstaFix({
endpoint: "/api/instafix",
projectName: "my-project",
onError: (error) => {
const code = (error as { code?: string }).code;
if (code === "AUTH") console.warn("check your apiKey");
},
});Branch on error.code, not instanceof — the error classes themselves are not exported from the package. When the user cancels the identity prompt, no error fires: that's a cancellation, not a failure.
Statuses, from the widget's side
Feedback has four statuses (open, in_progress, resolved, wont_fix). The widget displays all four but its own actions are deliberately binary: resolve and reopen. The in-between states belong to your triage workflow in the dashboard.
Reliability details you get for free
- Retry with backoff — failed submissions retry 3 times (10 s timeout, exponential backoff with jitter). Server errors and network failures retry; validation errors don't.
- Offline queue — when submission keeps failing, the payload is queued in localStorage (up to 20 entries) and flushed on the next page load. The queue stores payloads only — never tokens or headers, which are recomputed at flush time.
- Both apply to HTTP mode (
endpoint). In client-sidestoremode writes are local, so there is nothing to retry.