Skip to content

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 textContent or a validated attribute; innerHTML is banned in that codebase and a test enforces it. URLs must be https: 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:

<button onclick="window.withfeedback.push(['open'])">Send feedback</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.