@outcode/bug-reporter-web
v2.1.1
Published
In-app bug reporter for any web frontend (React, Angular, Vue, Svelte, Solid, vanilla). Framework-agnostic vanilla-DOM widget: a floating button that captures the screen, lets users annotate it (draw, arrow, box, blur, text), set severity/type, and file t
Downloads
553
Maintainers
Readme
@outcode/bug-reporter-web
In-app bug reporter for any web frontend. One floating button that captures the screen, lets users annotate it (draw · arrow · box · blur · text), set a severity and type, and file a report to ClickUp or any custom backend — implemented in framework-agnostic vanilla TypeScript/DOM.
A single package that works in React, Angular, Vue, Svelte, Solid, and plain HTML — mount it with
mountBugReporter(). For React Native, see
@outcode/bug-reporter-native.
Part of the OutCode Bug Reporter monorepo.
v2 is a framework-agnostic rewrite (breaking). v1 exported a React
<BugReporterButton>(peer depsreact/react-dom); v2 has no peer deps and exports the vanillamountBugReporter(). See Framework integration for the ~15-line React equivalent.
Features
- 🌐 One package, every web framework — React, Angular, Vue, Svelte, Solid, or plain HTML
- ✏️ Annotate — pen, arrow, box, blur/redact, and text, flattened onto the screenshot
- 🧭 Auto-captured context (consent-free) — route, browser/OS, viewport, language, connection type, console errors…
- 🎨 Themeable — Indigo / Noir / Mint presets or your own tokens
- 📡 Offline retry queue, screenshot size guard
- 🧩 Pluggable backends — ClickUp out of the box, or any
repository/ HTTP endpoint - 📦 No peer deps;
html2canvas-proinstalls automatically; ESM + CJS, fully typed
Install
npm install @outcode/bug-reporter-web @outcode/bug-reporter-coreUse
import { mountBugReporter } from '@outcode/bug-reporter-web';
import { OutcodeBackendBugReporterRepository, type BugReporterConfig } from '@outcode/bug-reporter-core';
const handle = mountBugReporter(
{
appName: 'My App',
theme: 'indigo', // 'indigo' | 'noir' | 'mint' | custom tokens
// Recommended: reports go through the OutCode bug service, so no ClickUp token ships in the client.
repository: new OutcodeBackendBugReporterRepository({
endpoint: 'https://your-bug-service.example.com/api/bugs/ingest', // your bug service URL
apiKey, // a scoped `bug:create` key
targetKey, // the project routing key from the bot's "Bug Trends" tab
}),
// Internal/trusted builds can talk to ClickUp directly instead (ships the token in the client):
// repository: new ClickUpBugReporterRepository({ apiKey, listId }),
} satisfies BugReporterConfig,
{ label: 'Report a bug' }
);
// Later:
handle.open(); // start the flow programmatically
handle.update(nextConfig); // e.g. after a theme change
handle.destroy(); // remove the widgetmountBugReporter(config, options?) appends a floating button to options.target (default
document.body) and returns a { open, close, update, destroy } handle. It no-ops safely in
non-browser (SSR) environments.
Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| label | string | 'Report a bug' | Accessible label for the floating button. |
| target | HTMLElement | document.body | Where to mount. Must have no transformed/filtered ancestor (else the fixed overlays are clamped). Mount-time only. |
| buttonStyle | Record<string,string> | — | Inline style overrides for the floating button. Wins over config.fabColor; note it styles the button only, not its halo. |
All configuration types (BugReporterConfig, ClickUpBugReporterRepository, …) come from
@outcode/bug-reporter-core.
Reporting a bug while a modal or dropdown is open
This works out of the box, including the awkward cases:
- Modal libraries that set
body{pointer-events:none}while open (Radix, MUI, Headless UI, shadcn) no longer disable the widget. - The flow renders in a
<dialog>opened withshowModal(), so it sits in the browser's top layer — above host modals, popovers, and anyz-index.Esccancels and hands control back to your modal. - The floating button is promoted to the top layer too, so a host overlay at
z-index:2147483647can't bury it. - When your app opens a modal
<dialog>(or marks the pageinert), the browser blocks everything outside that one subtree — no stacking trick can win. The widget relocates into that subtree for as long as it's open, then moves back. Your modal's own content stays fully interactive.
If the host modal is transformed (common with animation libraries), position: fixed resolves
against the modal rather than the viewport, so the button pins to the modal's corner instead of the
screen's. That's the one visible difference.
You can also drive it yourself — useful for a "Report a bug" menu item or a global shortcut:
const handle = mountBugReporter(config);
document.addEventListener('keydown', e => {
if (e.key === 'b' && (e.metaKey || e.ctrlKey)) handle.open();
});Framework integration
mountBugReporter is the same call everywhere — place it in your framework's mount hook and call
destroy() on teardown (and update(config) if your config is reactive).
React
import { useEffect } from 'react';
import { mountBugReporter } from '@outcode/bug-reporter-web';
function BugReporter({ config }) {
useEffect(() => {
const h = mountBugReporter(config);
return () => h.destroy();
}, []);
return null;
}Angular
import { Component, OnDestroy, OnInit } from '@angular/core';
import { mountBugReporter, type BugReporterHandle } from '@outcode/bug-reporter-web';
@Component({ selector: 'app-bug-reporter', template: '' })
export class BugReporterComponent implements OnInit, OnDestroy {
private handle?: BugReporterHandle;
ngOnInit() { this.handle = mountBugReporter(this.config); }
ngOnDestroy() { this.handle?.destroy(); }
}Vue
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { mountBugReporter } from '@outcode/bug-reporter-web';
let h;
onMounted(() => (h = mountBugReporter(config)));
onUnmounted(() => h?.destroy());
</script>Svelte
<script>
import { onMount } from 'svelte';
import { mountBugReporter } from '@outcode/bug-reporter-web';
onMount(() => {
const h = mountBugReporter(config);
return () => h.destroy();
});
</script>Solid
import { onMount, onCleanup } from 'solid-js';
import { mountBugReporter } from '@outcode/bug-reporter-web';
onMount(() => {
const h = mountBugReporter(config);
onCleanup(() => h.destroy());
});Vanilla / plain HTML — see Use above; just call mountBugReporter(config).
Theming
The three presets are a starting point — any colour works:
mountBugReporter({ appName: 'My App', theme: 'noir' }); // preset
mountBugReporter({ appName: 'My App', theme: { accent: '#FF5C00' } }); // custom accent
mountBugReporter({ appName: 'My App', theme: { preset: 'noir', accent: '#FF5C00' } });
mountBugReporter({ appName: 'My App', fabColor: '#FF5C00' }); // just the buttonA custom accent is enough on its own: accentPress, ring and onAccent are derived from it, so
the pressed state, focus rings and the button's bug glyph all follow — a light brand colour gets a
dark icon rather than white-on-white. Set any of those tokens explicitly to override the derived
value. An unparseable colour falls back to the preset instead of rendering an invisible button.
fabColor recolours the floating button and its halo only, leaving the rest of the flow on the theme
accent. Precedence for the button is buttonStyle (inline, mount option) → fabColor →
theme.accent.
Redaction
The blur tool paints an opaque block rather than a real blur, because a blur can sometimes be inverted and a password or a customer's name deserves better than that.
It also fails closed. Annotations are flattened onto the screenshot before it is submitted, and if that composite fails the original capture — which still shows what was redacted — is not sent as a fallback. The editor stays open with an error instead, and Continue retries. Reports with no redaction still fall back to the unflattened capture, since nothing was hidden to lose.
If you call the exported flattenAnnotations() yourself, note that it now rejects instead of
resolving with the original when it cannot composite a redaction — catch it rather than submitting
the source image.
License
MIT © OutCode Software
