Widget

Drop a floating feedback launcher and dialog into any web app with a single script tag — no iframe wrapper to build, no page to spin up.

The widget is a one-line install. Paste a <script> tag into your site and you get a floating Feedback pill in the corner that opens a feedback panel — submit ideas, browse posts, vote, comment. The same panel can also be opened from your own UI (a menu item, a help link) without the floating button.

This is the fastest way to put Boarbs in front of your users. If you'd rather embed the full board as a dedicated page inside your app, see Embed instead.

How the Widget Compares

WidgetEmbed (iframe)Portal
InstallOne <script> tagPage + iframe + backend sessionZero — hosted at your subdomain
Where it livesEvery page of your appA dedicated page inside your appA separate domain (yourco.boarbs.com)
AuthAnonymous or session tokenAlways session tokenAnonymous or portal sign-in
Best forAlways-on feedback, support, NPSA "Feedback" tab inside your appPublic roadmap, no-code launch

Quick Install (5 Minutes)

Paste before </body> on every page where you want feedback collection:

<script async src="https://YOUR_BOARBS_DOMAIN/widget/v1/widget.js"></script>
<script>
  window.Boarbs = window.Boarbs || (function () {
    var q = []; var f = function () { q.push(arguments); };
    f.q = q; f._stub = true; return f;
  })();

  window.Boarbs.init({
    workspace: "YOUR_WORKSPACE_SLUG", // optional if the script already loads from your workspace domain
    mode: "launcher",                 // "launcher" | "trigger"
    position: "bottom-right",         // "bottom-right" | "bottom-left"
    theme: { primaryColor: "#7c3aed" },
    locale: "en"
  });
</script>

The stub queue lets Boarbs.init(...) run before widget.js finishes loading — important when the loader is async.

YOUR_BOARBS_DOMAIN means the domain that serves your Boarbs instance or workspace. It is not the customer's app domain where you paste the snippet. On Boarbs Cloud this is typically a Boarbs-hosted workspace URL such as acme.boarbs.com. On a self-hosted install it is your own Boarbs domain.

How Workspace Resolution Works

  • If your script loads from a workspace domain such as acme.boarbs.com, the widget can resolve the workspace automatically.
  • If you want to be explicit, pass workspace: "acme".
  • If you want the widget pinned to one specific public board, pass boardId: "...".
  • If you pass a sessionToken, the widget resolves the board from that session. If the session was created without a boardId, Boarbs pins it to the first public board in the API key's workspace.

The widget renders a single feedback board inside a slide-in panel in your app. It uses the board from the sessionToken when present, the explicit boardId when provided, or the first public board in the workspace as a fallback.

When Changelog is enabled for the workspace, the panel also includes a Changelog tab.

The Two Modes

Launcher mode (default)

A floating pill in the corner opens the panel. Visible on every page where the script runs. Use this when you want feedback to be discoverable by default.

window.Boarbs.init({ workspace: "acme", mode: "launcher" });

Trigger mode

No floating button. The panel is opened programmatically from your own UI — a "Feedback" link in your user menu, a button in your settings page, anywhere.

window.Boarbs.init({ workspace: "acme", mode: "trigger" });

// In your own click handler:
document.getElementById("feedback-link").addEventListener("click", function () {
  window.Boarbs.open();
});

You can switch modes by calling init() again with different config. The widget is idempotent.

Anonymous vs Identified

Anonymous (default)

If you don't pass a session token, the widget runs anonymously. Visitors enter their email when they submit feedback, vote, or comment. The email is remembered in localStorage for the rest of the session, so they only enter it once.

This is the right mode for marketing sites, landing pages, or when you don't have user authentication.

Identified

Pass a sessionToken to attribute every action to a specific user — votes, comments, and posts are linked to their identity, and the email field disappears from the form.

The session token is created by your backend with your API key. Never put your API key in browser code.

// On your server:
const response = await fetch("https://YOUR_BOARBS_DOMAIN/api/embed/sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BOARBS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    boardId: process.env.BOARBS_BOARD_ID, // optional, but recommended when you have multiple public boards
    userId: user.id,
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName,
    avatarUrl: user.avatarUrl,
    plan: user.plan,
    expiresInSeconds: 2592000,
  }),
});

const { sessionToken } = await response.json();

If you omit boardId, the session is created against the first public board in the API key's organization.

When you create identified sessions, use a stable userId whenever possible. Boarbs prefers userId over email for matching returning users and falls back to email when needed.

This is the recommended pattern:

  • send userId for every identified user
  • send email as well when available
  • keep userId stable even if the user later changes their email address
  • pass avatarUrl when you want the widget to show the user's avatar

avatarUrl is optional. If you do not want profile images in the widget, simply omit it. If you do pass one, prefer a first-party or proxied image URL if privacy matters in your environment.

Pass it into the widget at init, or after sign-in:

// At init:
window.Boarbs.init({ workspace: "acme", sessionToken: "..." });

// After a user signs in mid-session:
window.Boarbs.identify("...");

The session token is the same one used by the iframe Embed flow — same endpoint, same fields, same lifetime rules.

Identifying a widget user also does not create a portal sign-in account by itself. To open the hosted portal as that same person, use Automatic Login — the widget attaches ?sso= to portal links, and Boarbs.portalUrl(path) builds those URLs for you.

JavaScript API

CallBehavior
Boarbs.init(config)Mount or reconfigure. Idempotent.
Boarbs.open(tab?)Open the panel. Optional "feedback", "posts", or "changelog".
Boarbs.openPost(postId, { upvote? })Open a post directly. Set upvote: true to add the current user's vote only when it does not already exist.
Boarbs.close()Close the panel.
Boarbs.toggle(tab?)Toggle.
Boarbs.identify(token)Switch to identified mode at runtime.
Boarbs.portalUrl(path?)Hosted portal URL with ?sso= when a session token is present.
Boarbs.destroy()Remove launcher and panel from the DOM.
Boarbs.hasUnseenChangelog()true while the launcher unread dot is shown.
Boarbs.on(event, fn)Subscribe to ready | open | close | submit | error | unread.

Lifecycle Callbacks

Pass these into init():

window.Boarbs.init({
  workspace: "acme",
  onOpen: () => console.log("opened"),
  onClose: () => console.log("closed"),
  onSubmit: (post) => console.log("submitted", post),
});

Open a Tab Directly

window.Boarbs.open("posts");
window.Boarbs.open("changelog");

Boarbs.open("changelog") opens the Changelog tab when the module is on.

Changelog unread

When Changelog is on, the launcher shows a red dot while the visitor has unseen entries. Opening Changelog — the tab or Boarbs.open("changelog") — clears it.

window.Boarbs.on("unread", ({ hasUnseen }) => {
  // same state as the launcher dot
});

window.Boarbs.hasUnseenChangelog(); // boolean

Deep-link to a Specific Post

Open the widget straight on a post from a button in your app. Pass the post's Boarbs ID:

window.Boarbs.openPost("POST_ID");

For an identified user, you can also ensure it is upvoted. This is idempotent: if that user has already voted, the existing vote stays in place. Anonymous visitors need a previously remembered email before a vote can be attributed.

window.Boarbs.openPost("POST_ID", { upvote: true });

Customization

OptionDefaultNotes
workspacescript domain's workspaceWorkspace slug. Optional when widget.js is already served from your workspace domain, or when a sessionToken is present. Used as the fallback board lookup scope when boardId is omitted.
boardIdfirst public boardExplicit public board target. Recommended when a workspace has more than one public board.
mode"launcher""launcher" shows the floating pill, "trigger" doesn't.
position"bottom-right"Or "bottom-left". Launcher mode only.
sessionTokennoneIdentified mode. Anonymous when omitted.
theme.primaryColorworkspace brand colorHex string. Drives the pill, panel accent, focus ring.
launcher.icon"default""default" | "chat" | "lightbulb" | URL to a 24×24 SVG/PNG.
launcher.label"Feedback"Text shown on the pill (also used for aria-label and tooltip).
launcher.showLabeltrueSet to false for an icon-only circular launcher.
tabs["posts"]Controls whether the panel exposes just the ideas list or both the ideas list and the submit view.
defaultTab"posts"First-open intent. Use "feedback" to open the new-request flow immediately, or "posts" to open the request list first.
localehost <html lang>BCP-47 string. Sets <html lang> inside the panel.

Reconfigure at Runtime

init() is idempotent. Calling it again with new config rebuilds the launcher and reloads the panel — useful when your app's theme changes after sign-in.

// Initial:
window.Boarbs.init({ workspace: "demo", theme: { primaryColor: "#7c3aed" } });

// User toggles dark mode:
window.Boarbs.init({ workspace: "demo", theme: { primaryColor: "#a78bfa" } });

Identifying Users After Sign-In

If your visitor signs in while the widget is already running, hand the new session token to the widget — no page reload needed:

// After your sign-in flow returns a Boarbs session token from your backend:
window.Boarbs.identify(sessionToken);

The panel reloads with the user's identity attached. The email field disappears from the feedback form, and votes / comments / posts attribute to that user.

React Example

A small client component that wires up the widget on mount:

"use client";

import Script from "next/script";
import { useState } from "react";

declare global {
  interface Window {
    Boarbs?: {
      init: (config: Record<string, unknown>) => void;
      open?: (tab?: string) => void;
      openPost?: (postId: string, options?: { upvote?: boolean }) => void;
    };
  }
}

export function BoarbsWidget({ workspace }: { workspace: string }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <Script
      src="https://YOUR_BOARBS_DOMAIN/widget/v1/widget.js"
      strategy="afterInteractive"
      onLoad={() => {
        window.Boarbs?.init({
          workspace,
          mode: "launcher",
          position: "bottom-right",
        });
        setLoaded(true);
      }}
    />
  );
}

Content Security Policy

If your site uses a CSP, allow the widget origin in three directives:

script-src  'self' YOUR_BOARBS_DOMAIN;
frame-src   'self' YOUR_BOARBS_DOMAIN;
connect-src 'self' YOUR_BOARBS_DOMAIN;

The panel iframe is sandboxed (allow-scripts allow-same-origin allow-forms allow-popups). The loader's only DOM footprint is a single launcher button and a single iframe container — it never reads or modifies the rest of your page.

Versioning

The script URL pins to a major version: /widget/v1/widget.js. Non-breaking changes ship in place — keep your snippet exactly as written and you'll receive improvements automatically. A breaking change ships under a new path (/v2/...).

Performance

  • Loader: under 15 KB gzipped.
  • Iframe: loads only on first open. Visitors who don't click pay zero panel weight.
  • Mobile (under 640px viewport): full-screen panel using 100dvh to dodge iOS Safari's address bar.
  • prefers-reduced-motion: animations are skipped at runtime, not via CSS — so they're truly off, including hover.
  • @media print: launcher and panel are hidden.

Accessibility

  • Launcher: aria-label, aria-haspopup="dialog", focus ring with brand-tinted color.
  • Panel: role="dialog", aria-modal="true", labelled by the workspace name.
  • Keyboard: opens with Enter/Space on the launcher; ESC closes the panel and returns focus to the launcher; Tab order is contained inside the iframe.

Troubleshooting

The launcher doesn't appear. Check the browser console. Confirm the script URL is reachable, your CSP allows it, and the widget can resolve the workspace either from the script domain, workspace, or sessionToken.

The panel opens but is blank. The workspace lookup failed, or the workspace has no public boards. Check the workspace slug or the Boarbs domain you're loading from.

Voting and commenting don't work. For anonymous visitors, vote and comment require an email address — the widget prompts for one on first interaction and stores it in localStorage. If your CSP blocks connect-src to the widget domain, the requests will silently fail.

The widget shows "Session expired". Your session token has expired. Either request a new one from your backend and call Boarbs.identify(newToken), or fall back to anonymous mode by removing sessionToken from init().

A returning identified user cannot undo an old vote. Check that your backend is issuing the same stable userId as before. Boarbs prefers userId for returning-user matching and only falls back to email when needed.

Multiple init() calls create duplicates. They shouldn't — init() is idempotent. If you see a duplicate launcher, you've loaded widget.js twice. Check that only one script tag points at it.

When to Use the Widget vs Other Options

Use the widget when:

  • You want feedback discoverable on every page of your product
  • You want install time measured in minutes
  • You're fine with the panel being a corner sheet (not a full page)

Use the Embed iframe when:

  • You want a dedicated "Feedback" page inside your app at full size
  • You want to pass URL filters (status, search, sort) on initial load
  • You're building a deeper integration that needs server-rendered embed sessions

Use the Portal when:

  • You want zero install — Boarbs hosts the page at your-workspace.boarbs.com
  • You want SEO and a public roadmap as part of your marketing surface

Next Steps

  • Embed — Full-page iframe with server-created sessions
  • Automatic Login — Carry widget identity onto the hosted portal
  • Portal — The hosted public site
  • API — Reference for the /api/embed/sessions endpoint and the rest of the public API
  • Permissions and Visibility — How public boards, embed sessions, and identified users interact
  • Troubleshooting — Common problems and where to look
Boarbs© 2026 Boarbs