The widget¶
One script tag. It both collects and displays, depending on the widget type you point it at.
<script async
src="https://withfeedback.fra1.cdn.digitaloceanspaces.com/widget/v1/widget.js"
data-wf-key="YOUR_PUBLIC_KEY"
data-wf-widget="YOUR_WIDGET_ID"></script>
Copy the exact snippet from the widget's page in the dashboard — it fills in both ids and the current bundle URL.
Types¶
| Type | What the visitor sees |
|---|---|
wall |
A grid of approved testimonials |
carousel |
The same content, one at a time, optionally auto-advancing |
collect_feedback |
A launcher button; clicking opens a feedback form |
collect_survey |
A launcher that opens one of your surveys |
nps_popover |
A launcher that asks the NPS question |
A display widget renders where you put the tag. A collect widget adds a fixed
launcher in a corner instead, so the tag can go anywhere — the end of <body>
is a good place.
What it does to your page¶
Deliberately very little:
- Shadow DOM. Everything renders inside a shadow root with
all: initial, so your CSS cannot leak in and ours cannot leak out. No reset to fight, no specificity war. - No dependencies, no framework, no globals beyond
window.withfeedback. - ~9 KB gzipped, with a size budget enforced in CI.
async, and every failure is silent. If our API is unreachable your page is unaffected — you get no widget, not a broken page.- No cookies. All requests go out with
credentials: "omit". - Text only. Every dynamic value is assigned with
textContentor a validated attribute;innerHTMLis banned in that codebase and a test enforces it. URLs must behttps:or they are dropped.
Attributes¶
| Attribute | Required | Purpose |
|---|---|---|
data-wf-key |
yes | Your project's public key |
data-wf-widget |
yes | Which widget to render |
data-wf-api |
no | Point at a different API host (self-hosting, staging) |
data-wf-preview |
no | Render but never write — used by the dashboard preview |
Two snippets on one page work. Two collect widgets in the same corner stack instead of hiding each other.
The JavaScript API¶
Available on every page with a collect widget:
<script>
window.withfeedback = window.withfeedback || [];
window.withfeedback.push(["identify", {
name: "Jane Roe",
email: "jane@example.com",
}]);
</script>
That is the documented bootstrap, and calls made before the bundle loads are queued and replayed — so you can push straight after the tag without waiting.
| Call | Effect |
|---|---|
["identify", {name, email}] |
Prefills the form. Saves a signed-in user typing what you already know |
["open"] |
Opens any collect widget on the page |
["open", "nps"] |
Opens one, by widget id or by type prefix |
open is how you trigger feedback from your own button:
identify is a convenience, not an identity claim
It prefills fields the visitor can still edit, and the server treats the result as visitor-supplied. It is not a way to assert verified identity — that needs a signed token, which is not available yet.
Displaying¶
Wall and carousel render approved, consented items only. That rule lives in one query in the server, and the widget never decides it — so an embed cannot be made to show a pending item by tampering with the client.
Video cards use preload="none" and never autoplay. A wall of ten video
testimonials pulls down ten posters, not ten videos.
Configuration¶
Everything visual is set in the dashboard and travels as validated data, never as CSS or HTML text: columns (1–4), accent colour (hex only), avatars, ratings, dates, autoplay interval, launcher label, corner. A content filter can narrow an embed by kind, minimum rating or tags — so a "5 stars only" wall is a configuration, not a separate integration.
Collecting video¶
A collect_feedback widget can record video in the browser, on your own domain.
See Video.