Skip to content

Install the widget

Add the feedback widget to your site in one step: paste the embed snippet. The widget appears immediately in anonymous mode. Call identify later to attribute activity to a specific user.

Embed the snippet

Paste this snippet before the closing </body> tag on any page where you want the widget:

<script>
  (function(w,d){if(w.Quackback)return;w.Quackback=function(){
  (w.Quackback.q=w.Quackback.q||[]).push(arguments)};
  var s=d.createElement("script");s.async=true;
  s.src="https://feedback.example.com/api/widget/sdk.js";
  d.head.appendChild(s)})(window,document);
 
  Quackback("init");
</script>

Replace https://feedback.example.com with your Quackback instance URL.

The snippet is also available in Admin → Settings → Widget with your instance URL pre-filled.

The snippet creates a lightweight command queue, then loads the SDK asynchronously. Commands called before the SDK loads are replayed automatically.

The widget appears for every visitor the moment the snippet loads. To attribute feedback to logged-in users, see Identify users.

Configure with init options

Pass options to Quackback("init") to customize the widget:

<script>
  Quackback("init", {
    placement: "left",
    defaultBoard: "feature-requests",
    buttonColor: "#6366f1"
  });
</script>
OptionTypeDefaultDescription
placement"left" | "right""right"Which corner the launcher button and panel appear in
defaultBoardstring-Board slug to filter posts. Omit to show all boards
buttonColorstring (hex)Theme primaryOverride the launcher button background color
launcherbooleantrueSet to false to hide the default floating button
tabsobjectAll enabledControl visible tabs: { feedback: true, changelog: true, help: false }
localestringAuto-detectedForce a locale: "en", "fr", "de", "es", or "ar". Omit to auto-detect from the browser
identityobject-Bundle identity at init time. Accepts the same payloads as identify{ id, email, name } or { ssoToken }. Omit for anonymous. Equivalent to calling init and then identify back-to-back

When launcher is false, no floating button appears. Use Quackback("open") from your own UI to open the widget programmatically:

Quackback("init", { launcher: false });
 
// Your own button opens the widget
document.getElementById("feedback-btn").addEventListener("click", () => {
  Quackback("open");
});

SDK commands

The Quackback() function accepts the following commands:

init

Initialize the widget. Call this once on page load. The widget appears immediately in anonymous mode. If you already know who the user is, pass an identity key to skip a separate identify call:

Quackback("init");
Quackback("init", { placement: "left", defaultBoard: "bugs" });
 
// Init + identify in one call
Quackback("init", {
  identity: { id: "user_123", email: "jane@example.com", name: "Jane" },
});

identify

Associate the current widget session with a user. Call once you know who the user is — you don't need to call it for anonymous visitors. See Identify users for the full guide.

For signed-in users, pass their details:

Quackback("identify", {
  id: "user_123",
  email: "jane@example.com",
  name: "Jane"
});

Or, with a server-signed token (recommended for production — see Identify users):

const { ssoToken } = await fetch("/api/widget-token", { method: "POST" }).then(r => r.json());
Quackback("identify", { ssoToken });

Calling identify again with different details switches the active user.

logout

Clear the current identity. The widget stays visible in anonymous mode. Call from your logout handler.

Quackback("logout");

open

Programmatically open the widget panel. You can pass options to open to a specific view:

// Open to the home view (default)
Quackback("open");
 
// Open directly to the new post form
Quackback("open", { view: "new-post" });
 
// Open with a pre-filled title and board
Quackback("open", { view: "new-post", title: "Bug: page crashes on save", board: "bugs" });
OptionTypeDefaultDescription
view"home" | "new-post" | "changelog""home"Which view to show
titlestring-Pre-fill the post title (only with view: "new-post")
boardstring-Board slug to pre-select (only with view: "new-post")

close

Programmatically close the widget panel.

Quackback("close");

destroy

Remove all widget DOM elements and reset state. Use this for cleanup in single-page apps when navigating away from pages that use the widget.

Quackback("destroy");

on

Subscribe to widget events. Returns an unsubscribe function. See Listen to widget events for all available events.

const unsubscribe = Quackback("on", "post:created", (payload) => {
  console.log("New post:", payload.title);
});
 
// Later:
unsubscribe();

off

Remove event listeners. Pass a specific handler to remove it, or omit the handler to remove all listeners for that event.

Quackback("off", "post:created", myHandler); // Remove specific handler
Quackback("off", "post:created");            // Remove all handlers

metadata

Attach key-value context to the widget session. Metadata is stored on posts created through the widget. See Attach session metadata for the full guide.

Quackback("metadata", { page: "/settings", app_version: "2.4.1" });

Single-page app usage

In React, Vue, or other SPA frameworks, initialize the widget in a layout component and clean up on unmount:

import { useEffect } from "react";
import { useAuth } from "@/hooks/use-auth";
 
export function FeedbackWidget() {
  const { user } = useAuth();
 
  useEffect(() => {
    Quackback("init", { placement: "right" });
    return () => Quackback("destroy");
  }, []);
 
  useEffect(() => {
    if (user) {
      fetch("/api/widget-token", { method: "POST" })
        .then((res) => res.json())
        .then(({ ssoToken }) => {
          Quackback("identify", { ssoToken });
        });
    }
    // No else — the widget is already anonymous after init.
  }, [user]);
 
  return null;
}

Call identify() separately from init(), after your auth state resolves. The SDK queues commands and replays them once the iframe is ready.

Launcher button

The widget renders a circular floating button (48px) with a chat bubble icon in the bottom corner of the viewport. It uses your theme's primary color by default, or the buttonColor you specify.

The button:

  • Appears immediately after Quackback("init") runs
  • Lifts slightly on hover with a shadow transition
  • Toggles between a chat icon (closed) and a close icon (open) with a smooth rotation
  • Stays visible while the panel is open on desktop so users can toggle it closed
  • Hides on mobile when the panel is open (the panel has its own close button)
  • Adapts to light/dark mode based on your theme settings

Mobile behavior

On viewports under 640px, the widget switches from a desktop popover to a full-screen overlay:

  • The panel slides up from the bottom with a backdrop overlay
  • A close button appears inside the panel header
  • The launcher button hides while the panel is open
  • Closing the panel slides it back down and restores the launcher button

This happens automatically based on viewport width — no configuration needed. If the user resizes the browser across the 640px breakpoint while the panel is open, the layout transitions smoothly.

Next steps