@randrusname/support-widget
v0.2.0
Published
Embeddable React support widget with webhook delivery, safe diagnostics, and optional attachment support.
Downloads
287
Maintainers
Readme
@randrusname/support-widget
Embeddable React support widget for collecting issue reports and sending them to
your webhook as multipart/form-data.
It is designed as a lightweight reusable library rather than app-local code:
- explicit CSS import
- React 18/19 support
- controlled or uncontrolled dialog state
- optional attachment support
- safe diagnostics defaults with redaction
- typed lifecycle callbacks for integrations
When to use it
Use this package when you need a simple in-app support / bug-report entry point that can be embedded into multiple React projects and delivered to your own backend, webhook receiver, or automation flow.
Install
npm install @randrusname/support-widgetPeer dependencies:
reactreact-dom
Quick start
import { SupportWidget } from "@randrusname/support-widget";
import "@randrusname/support-widget/styles.css";
export function App() {
return (
<SupportWidget
webhook={{
url: "https://example.com/webhook",
headers: {
"x-widget-source": "my-app"
}
}}
context={{
app: "crm",
userId: "42",
environment: "production"
}}
/>
);
}Full example
import { useState } from "react";
import {
SupportWidget,
type SupportWidgetSubmitErrorDetails
} from "@randrusname/support-widget";
import "@randrusname/support-widget/styles.css";
export function App() {
const [isOpen, setIsOpen] = useState(false);
function handleSubmitError(details: SupportWidgetSubmitErrorDetails) {
console.error("Support widget submit failed", details.error);
}
return (
<SupportWidget
webhook={{
url: "https://example.com/webhook",
headers: {
"x-widget-source": "customer-portal"
}
}}
open={isOpen}
onOpenChange={setIsOpen}
onSubmitError={handleSubmitError}
accept={["image/*", ".txt", ".log"]}
maxFileSizeMb={5}
context={{
app: "customer-portal",
userId: "42",
tenantId: "acme"
}}
diagnostics={{
includePayloads: true,
includeResponses: true,
maxPayloadLength: 8000
}}
labels={{
buttonLabel: "Contact support",
dialogTitle: "Report a problem"
}}
/>
);
}Styles
Styles are not injected automatically. Import them explicitly:
import "@randrusname/support-widget/styles.css";The widget exposes CSS custom properties with built-in fallbacks:
--sw-bg--sw-fg--sw-muted--sw-border--sw-accent--sw-accent-contrast--sw-overlay--sw-radius--sw-shadow--sw-surface--sw-font-family
Public API
Props
webhook: { url: string; headers?: Record<string, string> }Required destination for the multipart request.context?: Record<string, string | number | boolean | null>Optional flat metadata appended ascontext.*fields.defaultOpen?: booleanInitial dialog state for uncontrolled usage.open?: booleanControlled dialog state.onOpenChange?: (open: boolean) => voidCalled when the widget requests an open-state change.onOpen?: () => voidCalled when the dialog actually becomes visible.onClose?: () => voidCalled when the dialog actually closes.onSubmitStart?: (details) => voidCalled after validation and before the request is sent.onSubmitSuccess?: (details) => voidCalled after a successful2xxresponse.onSubmitError?: (details) => voidCalled when the request fails or the webhook returns a non-2xxresponse.accept?: string | string[]Native file accept rules. The widget also validates selected files against these rules before submission.maxFileSizeMb?: numberMaximum single attachment size. Default:10.diagnostics?: SupportWidgetDiagnosticsConfigControls console/network diagnostics capture.labels?: Partial<SupportWidgetLabels>Text overrides for the built-in UI.buttonLabel?: stringDeprecated alias forlabels.buttonLabel.zIndex?: numberOverride stacking order. Default is a very high widget-friendly value.
Callbacks
onSubmitStart, onSubmitSuccess, and onSubmitError receive:
descriptionattachmentcontextdiagnosticsPayloadformData
Success callbacks also receive response, and error callbacks receive error.
Payload format
The widget submits multipart/form-data with these fields:
descriptionappTaken fromcontext.appwhen present.submittedAtISO timestamp created on submit.context.*Flattened context fields such ascontext.userId.context.page.urlSanitized current page URL without query string or hash.context.page.routeCurrent pathname.context.client.browsercontext.client.oscontext.client.mobilediagnosticsCompact JSON string, or literalnullwhen no diagnostics are included.attachmentA single file, when selected.
Example diagnostics payload:
{
"c": [
{
"l": "e",
"m": "Request failed with token=[REDACTED]",
"t": "2026-03-24T10:00:00.000Z"
}
],
"n": [
{
"x": "f",
"m": "POST",
"u": "https://api.example.com/checkout",
"s": 500,
"d": 124,
"t": "2026-03-24T10:00:01.000Z",
"p": "{\"token\":\"[REDACTED]\",\"step\":\"checkout\"}",
"r": "{\"message\":\"failed\"}"
}
]
}Attachments
- Only one attachment is supported in this version.
- Files are validated against both
acceptandmaxFileSizeMb. - File contents are sent only as the final
attachmentmultipart field. - Diagnostics capture never includes raw file contents. File references are reduced to metadata such as name, type, and size.
Diagnostics and privacy
Diagnostics are enabled by default, but the default mode is intentionally conservative:
- console capture:
warnanderror - network capture: failed
fetch/XMLHttpRequestcalls - request URLs are sanitized to
origin + pathname - request payloads are not captured unless
includePayloads: true - response bodies are not captured unless
includeResponses: true - sensitive values are redacted by default
- diagnostics payload size is capped
- the widget webhook request itself is excluded from diagnostics output
- cookies and request headers are not collected
Default sensitive keys:
tokenaccess_tokenrefresh_tokensessioncookieauthorizationapi_keysecretpasswordkey
You can extend the redaction list with diagnostics.redactedKeys.
Diagnostics config
enabled?: booleancaptureConsole?: booleancaptureNetwork?: booleanincludePayloads?: booleanincludeResponses?: booleanmaxConsoleEntries?: numbermaxNetworkEntries?: numbermaxPayloadLength?: numberredactedKeys?: string[]
Accessibility notes
The widget includes:
- keyboard close via
Escape - focus return to the trigger button
- basic focus trapping inside the dialog
aria-invalidandrole="alert"for validation errors- semantic
dialogmarkup
Troubleshooting
- Import styles explicitly. The package does not auto-inject CSS.
- If you use controlled mode, make sure
onOpenChangeupdates youropenstate. Otherwise the dialog will not visually open or close. - If files are rejected, verify both
acceptandmaxFileSizeMb. - If you enable
includePayloadsorincludeResponses, remember that redaction is best-effort and should complement, not replace, server-side controls.
Compatibility
- React
^18.3.0 || ^19.0.0 - ESM and CommonJS entry points are exported
- SSR-safe render behavior: the widget returns
nullwhendocumentis not available
Migration notes
buttonLabelstill works, but it is deprecated. Preferlabels.buttonLabel.- Styles must continue to be imported explicitly from
@randrusname/support-widget/styles.css. - The diagnostics default is now safer: payloads and response bodies are excluded unless explicitly enabled.
Versioning
This package is intended to follow semver once the public API stabilizes. Until then, review release notes carefully when upgrading between early versions.
