Hopp til innholdet

DokumentasjonChat-widgetenMarkdown

Visitor widget

The stock chat bubble. One script tag, no build step, no CSS to import.

html
<script src="https://<host>/widget.js" data-tenant="<tenantId>" defer></script>

Requirements:

  1. The page's origin (e.g. https://www.example.no) is listed in the tenant's allowed origins (admin → Innstillinger → Nettsteder som kan vise chatten, or PATCH /api/v1/operator/tenant { allowedOrigins }). http://localhost:* works outside production.
  2. That's it. Name, portrait, welcome text, suggestions and team come from the tenant settings.

The widget renders inside a shadow root, so your CSS can't break it and its CSS can't leak into your page.

Options (data-* attributes)

AttributeDefaultMeaning
data-tenantrequiredTenant id. (data-kunde is an accepted alias.)
data-positionrightleft puts the launcher bottom-left.
data-openfalsetrue opens the chat on load.
data-languagebrowser languageForce the reader language (no, en, …).
data-accenttenant brand colourAccent colour override, #rrggbb.
data-user-name, data-user-email, data-user-phone, data-user-companyWho the visitor is, if your page knows (e.g. rendered by your server for a logged-in user). Same as Snakkbar.setUser(...), see below.

JavaScript API

js
Snakkbar.open();
Snakkbar.close();
Snakkbar.toggle();

// Switch to an identified visitor (token minted by your backend, see below).
Snakkbar.identify(async () => (await fetch("/api/snakkbar-token")).text());

// Tell Snakkbar who the visitor is (all fields optional; calls are merged).
Snakkbar.setUser({
  name: "Kari Nordmann",
  email: "kari@example.no",
  phone: "+47 900 00 000",
  company: "Nordmann AS",
  attributes: { customerNo: 1234, plan: "pro" }, // your own fields; null removes one
});

// Events: "open", "close", "message" (a stored Message), "unread".
const off = Snakkbar.on("message", (message) => console.log(message.role, message.body));
off();

Snakkbar.destroy(); // remove the widget completely

Mounting by hand (SPAs that load the script without data-tenant, or several tenants on one page):

js
// after <script src="https://<host>/widget.js"> has loaded
const widget = window.Snakkbar.mountWidget({
  host: "https://<host>",
  tenantId: "<tenantId>",
  position: "right",
  user: { name: "Kari Nordmann", email: "kari@example.no" }, // optional, same shape as setUser
});
widget.setUser({ company: "Nordmann AS" });

A launcher badge shows unread assistant/staff messages while the chat is closed.

Telling staff who the visitor is

Snakkbar.setUser({ name, email, phone, company, attributes }) (also WidgetController.setUser, MountWidgetOptions.user and the data-user-* attributes) sends what your page knows about the visitor to Snakkbar (PATCH /api/v1/visitor/me). Staff see it on the visitor and their conversations, the assistant doesn't ask for it again, and the callback form is filled in from it.

  • Call it after the widget script has loaded, whenever you learn something (e.g. after login). Calls made before the widget has mounted are queued; calls are merged.
  • After identify(token) it is applied again to the identified visitor. identify(null) (logout) forgets it, so the next anonymous visitor doesn't inherit it.
  • Field rules: api.md. Invalid values are rejected by the API; the widget only logs the error to the console and never throws.
  • It is unverified: anyone can type anything into a browser. For an identified visitor it only fills fields that are still empty and adds attribute keys that don't exist yet; trusted values belong in your backend's token request (POST /server/visitor-tokens takes name, email, phone, company, attributes).

Live visitors

On every page it is on, the widget reports the visitor as a live visitor, also before (and without) a chat: staff see who is on the site now, the current page, the navigation history, device, rough location and where the session came from, in the admin's «Besøkende» and «Oversikt». An operator can open «Se live» to follow the visitor's cursor, clicks and scroll over the page. This is the SDK's startTracking(); details in realtime.md, and what is and isn't collected in security.md.

The widget does nothing extra for this: no setup, and no visible change for the visitor. Inside an operator's live view (an iframe named snakkbar-mirror) the widget renders nothing and only keeps the page scrolled to where the visitor is.

Switch. Live visitors are on by default. An admin turns them off per tenant under Innstillinger (or PATCH /api/v1/operator/tenant { "liveVisitors": false }). Off, the widget sends nothing and creates no visitor until someone opens the chat; staff then only see visitors who chat.

Consent. Live visitors track behaviour on the site (pages, and the cursor while watched) and store a visitor token in localStorage for every browser that opens a page. In many jurisdictions (e.g. the EU/EEA under GDPR and the ePrivacy rules) that needs the visitor's consent unless your site has another legal basis. If your site needs consent for this kind of tracking, do one of:

  • No consent mechanism, or you don't want the tracking: turn liveVisitors off. The chat works as before; nothing is sent until the visitor opens it.

  • Consent banner: load the widget only after the visitor has consented (statistics/analytics category, or whatever your banner calls it), and mention Snakkbar in your privacy notice:

    js
    // e.g. in your consent manager's "accepted" callback
    function loadSnakkbar() {
      if (window.Snakkbar?.open) return;
      const s = document.createElement("script");
      s.src = "https://<host>/widget.js";
      s.dataset.tenant = "<tenantId>";
      document.body.appendChild(s);
    }

    Visitors without consent then have no chat either. If they should still be able to chat, turn liveVisitors off instead: the switch is per tenant, not per visitor.

Whether consent is required is the site owner's call; Snakkbar gives you the switch and keeps the data minimal.

Identified visitors

By default every browser is an anonymous visitor. If your site has logged-in users, let the chat follow them across devices and show staff who they are:

ts
// Your backend (never expose the secret key in the browser)
const res = await fetch("https://<host>/api/v1/server/visitor-tokens", {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.SNAKKBAR_SECRET_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ externalId: user.id, name: user.name, email: user.email, phone: user.phone, company: user.company }),
});
const { token } = await res.json(); // valid 24 h; send it to the browser

Then Snakkbar.identify(token) (or a function returning a fresh token). Call identify(null) on logout to return to an anonymous visitor.

Look and behaviour

  • Colours come from your page: shadcn/ui CSS variables on <html> if present, otherwise the page's background and text colours, with the tenant's brand colour (or data-accent) as accent. Light and dark pages are handled, and the widget follows live theme switches.
  • Launcher: shows the face of a staff member who is online (with a green dot), otherwise the assistant's portrait.
  • Desktop: a 440×720 popup in the corner. Visitors can resize it by dragging the top edge, the side edge or the corner; the size is remembered, and double-clicking a handle resets it.
  • Mobile (≤700 px): a full-screen sheet with a grab bar; keyboard-aware.
  • Chats: the ☰ menu lists the visitor's earlier conversations and starts a new one.
  • People: the team row and @name reach staff; the assistant hands over when asked.
  • Images: up to 4 per message (JPEG, PNG, WebP, GIF; 5 MB each).
  • Translation: messages in other languages show translated, with «Vis original».
  • Navigation: staff can move the visitor to another page on the same site.
  • Respects prefers-reduced-motion. Escape closes.

Content Security Policy

If your site sends a CSP, allow:

script-src https://<host>;
connect-src https://<host>;
img-src https://<host> https://*.supabase.co data: blob:;
style-src 'unsafe-inline';

For the operators' live view («Se live») your pages must also let the Snakkbar admin frame them: frame-ancestors 'self' https://<host> (plus the origin of your own admin if you embed it), and no X-Frame-Options header that excludes those origins. Without it the live view shows a schematic page instead. See admin-embed.md.