<HelpWidget /> from @userkit/react is the same panel as
code — with style control (unstyled), the inline arrangement, and the components
inside your own. For the other components by script — sign-in, account, pricing —
see Components by script.
What the page pays
One request, and the others only when somebody needs them:
The first carries the client, the button, and the decision to draw it. The
second is React and the panel. Somebody who never asks for help never downloads
it; somebody with an unread reply gets it fetched for them, because the preview
of that message is precisely the thing that cannot wait for a click — and
somebody whose pointer reaches the button gets it fetched a moment before the
click, which is what makes the panel open at once.
The last is the components, and it is small because everything it shares with
the panel — React, the client, the stylesheet — is the file above. A page with
no
[data-userkit] and no UserKit.mount() call never downloads it, and a
page that uses both the widget and a component downloads React once.
Installed from npm the panel is in your bundle and loads with the page. By script
it is fetched when needed — which makes this form lighter on most pages, not a
lesser one.
Where it draws
In a shadow root at the end of<body>. The host element and the <script>
tags we add to <head> are the only nodes of ours in your tree; everything
drawn is inside the boundary, and your CSS does not enter it — not a * { }
reset, not an !important on button. That is the one real difference from
the npm package, where your CSS reaches the components on purpose.
The button appears only when there is something behind it: published articles
(public, no session needed) or the chat with a contact session. Neither, and the
page is exactly as it was — a launcher in front of nothing is a button that opens
an apology.
The components, on the same tag
widget.js is not only the button in the corner: it mounts the same drop-ins as
Components by script, on the page that already
carries it.
<script> line to put a notice at the
top of a page that already had support on it. The names, the data-* and the
events are that guide’s, and data-auto="false" turns off automatic mounting here
too.
userkit.js still exists, and the difference is exactly one thing: it has no
floating button. A sign-in screen or a pricing page wants the components and
nothing in the corner; a product that already installed the widget does not need
it. Loading both on one page is harmless in either order — one client, one
session, one set of page views, and the widget adds the launcher on top of
whatever the other file already started. A second widget.js tag is ignored the
same way.The one rule is that both tags carry the same data-publishable-key. One
page is one environment: the session, the queued events and the stored keys all
belong to it, so a second key cannot be served alongside the first. The tag that
names a different one says so in the console and does nothing.Configuration
All by attribute:data-whats-new is the only one that ships off, and the reason is a real
trade-off: the tab reaches whoever opens the panel, the dialog reaches everybody —
at the cost of appearing over a page somebody opened to do something else. Turn it
on when what you publish needs to be read rather than merely available. The two do
not double up: being shown is what marks a note read, so whichever appears first
empties the other.
Light, dark, or whatever they chose
With nodata-theme, the button and the panel follow the reader’s
prefers-color-scheme — right on most pages, and what we have done for a long
time. It is not enough for an app that decides for itself: if its theme is a class
on <html>, the OS can say dark while the toggle in the header says light, and the
widget is then the one box on the screen in the wrong palette. CSS cannot fix that
either — the panel lives in a shadow root the page’s stylesheet cannot reach. Hence
an attribute.
@userkit/react puts <ChangelogBadge /> and <NotificationBell /> in its own
header, beside the avatar. This form has one button and one panel, and no header
to hang a bell in. Only for a proxy |
A page that would rather decide in code leaves the attributes off and calls init:
window.UserKit
show() works before the panel exists: it is fetched and then opens, which is what
makes a “Help” button in your own menu work without you waiting on anything.
Waiting for the script
The tag isdeferred, so your page has no way of knowing when it ran. ready() is
the answer: a promise resolving with the client as soon as this origin’s session
has been restored — or with null if there was no publishable key at all.
One button, not two
If your product already has a “Support” in its header,data-launcher="false"
takes ours out of the corner. The panel is untouched — UserKit.show() opens it
from your button, which is the whole reason to hide the launcher rather than not
install the widget.
For a decision made in code — “hide it while this modal is up” — use
hideLauncher(). A command outranks the attribute from then on, and
resetLauncher() is the only way back: an override that expired on its own would
be our button reappearing over somebody’s checkout for reasons nobody could
reconstruct.
With the panel open the button is drawn either way, because there it is not a
launcher — it is the panel’s own close.
Drawing your own button
subscribe publishes what the widget knows about itself, which is exactly what a
button of your own needs in order to draw itself:
available is the same answer that decides whether our button appears — a
button in front of nothing is a button that opens an apology. And unread is our
badge’s own number, counted once: a line the person wrote themselves is never
unread to them, and two independent counts would eventually disagree about that in
front of them.
Identifying the person on the page
The script never creates a contact on its own. A tag that quietly started minting rows for landing-page traffic would be the worst possible default — the decision is yours, here as in the npm package:hash
is what your server signed; never mint that HMAC in the browser.
CSP
If your site sends aContent-Security-Policy, the loader and the files it
fetches need script-src, the API needs connect-src, and the logo and avatars
your environment serves need img-src:
style-src of its own: inside the shadow root it is a
constructed sheet (adoptedStyleSheets), which that directive does not govern.
Two things still ride on style-src 'unsafe-inline' — a browser without
constructed sheets, and the dark palette set in the panel, which the components
render as a <style> inside their own tree. Without it the widget draws in
light, and nothing else is lost.
The crossorigin="anonymous" on the tag is what lets an error of ours reach your
window.onerror with a stack instead of as "Script error.", and what lets the
script load on a page that sends Cross-Origin-Embedder-Policy.
We deliberately publish no SRI hash for widget.js: it is the file that changes so
a fix reaches your page without you deploying anything, and a fixed hash is the
opposite of that. The panel is immutable instead — its name carries the hash of its
contents.
What only exists on npm
appearance.unstyled, and your own CSS over it- the
inlinearrangement, in the page’s flow useHelpWidget()and the rest of the hooks- passing functions and objects as props in JSX — here that is
UserKit.mount()or anaddEventListeneron the element