Controlling the widget
The widget works on its own, but you can also drive it from your own UI. Open specific sections, react to events, and replace the launcher entirely.
Opening the widget
open() with no arguments opens the widget where the user left off:
uj.open();Pass a target to open a specific place:
uj.open({ to: 'home' });
uj.open({ to: 'feedback' });
uj.open({ to: 'roadmap' });
uj.open({ to: 'updates' });
uj.open({ to: 'messages' });
uj.open({ to: 'notifications' });Some targets accept extra options:
// Open the feedback submission form directly
uj.open({ to: 'feedback', compose: true });
// Open a specific feedback post
uj.open({ to: 'feedback', submissionId: 'POST_ID' });
// Open a specific changelog update
uj.open({ to: 'updates', updateId: 'UPDATE_ID' });
// Start a message with prefilled text
uj.open({ to: 'messages', compose: { text: 'I need help with...' } });close() hides the widget and toggle() switches between open and closed:
uj.close();
uj.toggle();These are useful anywhere in your product. For example, a "Give feedback" item
in your account menu can call uj.open({ to: 'feedback', compose: true }), or
a "Contact us" button can call uj.open({ to: 'messages' }).
The messages target requires Conversations to be enabled in your workspace.
If it is not, the call reports a CONVERSATIONS_DISABLED error through the
error event and opens Home instead.
Listening to events
Subscribe with on. It returns a function that removes the listener:
const unsubscribe = uj.on('open', () => {
console.log('widget opened');
});
// later
unsubscribe();Available events:
| Event | When it fires |
|---|---|
ready | The SDK finished loading. Fires immediately if already loaded. |
open | The widget became visible. |
close | The widget was hidden. |
identify | The identity changed, including logout. |
unread | The unread count changed. Payload is { count }. |
presence | Team availability changed. Payload is { availability: 'online' | 'neutral' }. |
statechange | Any part of the widget state changed. |
error | Something went wrong. Payload is the error. |
Reading state
getState() returns a snapshot of the current widget state:
const state = uj.getState();
state.status; // 'ready' once the SDK is loaded
state.widget.open; // is the widget visible right now
state.unreadCount; // number, or null before the first fetch
state.presence; // 'online' when someone from the team is online
state.session.status; // 'anonymous', 'identifying', or 'identified'
state.session.viewer; // the identified user, or nullBuilding your own launcher
The flagship use case: hide the default button and use your own, with a live unread badge.
First, disable the default launcher:
uj.init('YOUR_PROJECT_ID', {
widget: { launcher: false }
});Hiding the launcher removes the button, but notification whispers — the small toasts announcing a new changelog post or a reply — keep appearing in the widget corner. That is deliberate: your button replaces the entry point, not the widget. See Whispers below for how to move or disable them.
Then wire up your button:
const button = document.querySelector('#feedback-button');
const badge = document.querySelector('#feedback-badge');
button.addEventListener('click', () => uj.toggle());
uj.on('unread', ({ count }) => {
badge.textContent = count > 0 ? String(count) : '';
});You can also mirror the widget's green "team is online" dot:
const dot = document.querySelector('#feedback-online-dot');
uj.on('presence', ({ availability }) => {
dot.hidden = availability !== 'online';
});'neutral' means the SDK cannot currently prove anyone is online — it never
claims the team is offline, so hide the dot rather than showing an "offline"
state. Read uj.getState().presence for the initial value.
While the widget is closed, presence refreshes on page load, on tab focus,
and every few minutes in between — so treat 'online' as "online as of the
latest check". While the widget is open it updates live.
That is all. The badge and dot update when new updates are published, new messages arrive, or team availability changes, the same way the default launcher does. Subscribing to either event keeps the data flowing in the background.
The unread count includes conversation replies alongside changelog and feedback notifications. When the user opens the widget with conversation unread pending, it lands on Messages first so the reply is the first thing they see.
If you just want the default launcher to open from an existing element on your page, you can also pass a selector instead of building it yourself:
uj.init('YOUR_PROJECT_ID', {
widget: { launcher: { selector: '#feedback-button' } }
});Whispers
Whispers are ephemeral toasts near the widget corner: "New: Dark mode is live", "Sarah replied to your idea". They appear for a few seconds, are clickable (opening the widget at the exact item), and dismiss themselves.
Whispers announce changelog posts and feedback notifications. Conversation replies do not whisper; they light up the unread count instead.
They are independent of the launcher. With the default launcher, whispers
expand out of the button itself. With launcher: false or a selector
launcher, they appear on their own in the corner — no button, no unread dot,
nothing while idle.
If your own button lives in the same corner, move the whisper surface out of
its way with offset:
uj.init('YOUR_PROJECT_ID', {
widget: {
launcher: false,
offset: { bottom: 84 } // pixels from the bottom edge; `side` works too
}
});To turn whispers off entirely — for example if you render your own
notifications from the unread event — disable them explicitly:
uj.init('YOUR_PROJECT_ID', {
widget: { launcher: false, whispers: false }
});With both disabled and no explicit update notification mode, the widget renders
nothing until you call uj.open().
Note that unread and presence events keep working either way as long as
you are subscribed.
Update announcements
Choose how the widget announces the latest unseen changelog entry with
widget.notifications.updates:
uj.init('YOUR_PROJECT_ID', {
widget: {
notifications: { updates: 'full' }
}
});| Mode | Behavior |
|---|---|
'whisper' | Announces the update in a corner toast using the existing repeat limits. |
'full' | Opens the update directly in the widget reader, including its cover image and full content. |
'none' | Suppresses update announcements while keeping the updates section and unread badge available. |
If you omit the setting, updates use 'whisper' unless whispers: false,
which defaults updates to 'none'. An explicit update mode takes precedence
for changelog announcements only. Feedback whispers still follow whispers,
and conversation replies still contribute to unread independently.
Full announcements work with a hidden or custom launcher and no event subscriptions. For example, this configuration opens unseen updates without showing a launcher or other whispers:
uj.init('YOUR_PROJECT_ID', {
widget: {
launcher: false,
whispers: false,
notifications: { updates: 'full' }
}
});On a first visit, full mode can show the latest published entry even if it is
old. The SDK attempts at most one automatic opening per initialization visit.
A manual open, toggle, or close prevents further automatic opening during that
visit. A failed or interrupted attempt also spends that allowance. Reloading
or calling init() again starts a new visit; changing identity or navigating
your app does not.
Automatic opening waits for a visible, focused page and a closed widget.
Losing focus or closing the widget during loading cancels that attempt.
widget: false disables automatic presentation entirely.
An article becomes seen when its content is mounted in the shown widget on a visible, focused page. This applies to manual and automatic views. A click, prefetch, failed load, or hidden article does not mark it seen. Reading an older entry does not consume a newer unread update. An entry with the stored seen ID stays seen if it is edited or republished.
Seen history is stored for the project in this host origin's browser storage. It is not synchronized across users, devices, browsers, or origins. Clearing or blocking storage can allow another announcement on a later visit, and simultaneous tabs can race.
Updates are discovered through the existing attention checks on initialization, focus, and roughly every five minutes while the widget is closed. Publishing an entry does not immediately push it into every open page.
Changing the language
The widget picks a language automatically. To set it yourself:
uj.init('YOUR_PROJECT_ID', { locale: 'fr-FR' });You can change it later without reloading:
await uj.setLocale('de-DE');