@statisfy/digital-workers-react
v0.1.9
Published
Embeddable Digital Worker chat SDK for React.
Downloads
463
Maintainers
Readme
@statisfy/digital-workers-react
Embeddable Digital Worker chat and project modules for React.
Install
npm install @statisfy/digital-workers-react react react-domreact and react-dom (18 or 19) are peer dependencies you provide. Everything
else the SDK needs is bundled.
Usage
Wrap your app once in <StatisfyProvider>, then render components anywhere
beneath it. Connection and auth live on the provider; components take only their
own display props.
import {
DigitalWorkerChat,
StatisfyProvider
} from '@statisfy/digital-workers-react'
import '@statisfy/digital-workers-react/styles.css' // required — import once
export function Support() {
return (
<StatisfyProvider
publishableKey="pk_live_…"
getToken={async () => '<bearer>'}
>
<DigitalWorkerChat workerId="<uuid>" workerName="Assistant" />
</StatisfyProvider>
)
}You do not configure a gateway URL — the SDK targets the Statisfy production
gateway. Mint the bearer on your own backend and return it from getToken;
never ship a secret to the browser.
A component rendered outside <StatisfyProvider> throws, rather than silently
sending unauthenticated requests. The stylesheet import is required; without it
the widget renders unstyled.
Theming
Everything visual in the SDK is reachable from host CSS through two seams — a variable table and a set of anatomy attributes. Both work with plain, normal declarations.
!important is not needed and must not be used. All SDK CSS ships inside
@layer (theme, base, components, utilities), and for normal declarations an
unlayered author rule outranks every layer regardless of specificity or import
order. !important inverts layer order, so it makes the SDK's own rules win
again — the opposite of what you want.
One exception, and it is not part of the theming contract. Under
prefers-reduced-motion: reduce the SDK sets transition-duration,
animation-duration, animation-iteration-count and scroll-behavior with
!important on [data-dw-part] and its descendants. That is an accessibility
floor, not a style: the person asking for less motion is the end user, not the
integrator, so it is the one axis a host does not get to overrule. Everything
you actually theme — color, radius, elevation, spacing, layout, and the motion
tokens themselves — stays plain-declaration territory where you always win.
The floor is scoped to [data-dw-part], so it can never reach your own DOM.
1. Override a variable
Declare it in an unlayered rule — :root, or any scope you want it confined to.
No !important, and it holds no matter which stylesheet loads first.
:root { --dw-primary: 13 148 136; }
/* or scope it to one mount point */
.support-drawer { --dw-primary: 13 148 136; }Colors are space-separated RGB triplets, not rgb() or hex — that is what
lets opacity modifiers such as dw:bg-destructive/10 work internally. Write
13 148 136, never #0d9488.
Colors
| variable | default | used for |
|---|---|---|
| --dw-background | 255 255 255 | primary surface |
| --dw-background-secondary | 248 248 249 | headers, inset surfaces |
| --dw-foreground | 26 26 26 | body text |
| --dw-muted | 241 241 242 | hover fills, agent bubble, badges |
| --dw-muted-foreground | 107 114 128 | secondary text, timestamps |
| --dw-primary | 83 35 181 | brand fill: send button, progress, current step |
| --dw-primary-foreground | 255 255 255 | text on --dw-primary |
| --dw-border | 229 231 235 | hairlines, dividers, table cells |
| --dw-input | 209 213 219 | form control borders |
| --dw-destructive | 220 38 38 | errors, failed tool calls |
| --dw-success | 22 128 90 | completed tool call, completed journey step |
| --dw-warning | 180 83 9 | "Awaiting reply", pending states |
Tinted grounds pair with text-foreground, never with their own hue. A
semantic hue is used one of three ways: as a solid fill with
--dw-primary-foreground-style inverse text on it, as an icon or dot at full
saturation, or as a low-alpha tint (bg-success/12) behind ordinary body text.
What it is never used for is small text on a tint of itself. At any mid-luminance
hue — and --dw-success, --dw-warning and --dw-destructive are all
mid-luminance by construction, because they have to read on white — the tint
lands only a few percent away from the type, so text-success on
bg-success/12 cannot reach 4.5:1 no matter which green you pick. Darkening the
token to fix the text then breaks the icon and the solid fill. So the hue
carries the meaning and the foreground token carries the legibility; if you
retheme a semantic color, you only have to keep it distinguishable, not
readable-on-itself.
Dark palette (0.1.5) — opt-in
The SDK now ships a measured dark palette, and you turn it on. Before 0.1.5
there was none, so dark mode was entirely your problem. It is opt-in rather than
automatic because an embedded widget should match the page it is mounted in,
not the visitor's OS — most host pages are light-only whatever the OS says, so
keying dark off prefers-color-scheme alone flips the widget dark inside a light
page. CSS cannot ask "is my host page dark?", so you tell it:
<html data-dw-theme="dark"> <!-- always dark -->
<html data-dw-theme="auto"> <!-- follow the visitor's OS -->
<!-- attribute absent --> <!-- light: the 0.1.x behaviour, unchanged -->Nothing changes appearance when you upgrade. If you already declare a full
palette you need none of this — your unlayered :root outranks both blocks.
| variable | light | dark |
|---|---|---|
| --dw-background | 255 255 255 | 42 43 53 |
| --dw-background-secondary | 248 248 249 | 52 53 65 |
| --dw-foreground | 26 26 26 | 237 238 243 |
| --dw-muted | 241 241 242 | 60 61 74 |
| --dw-muted-foreground | 107 114 128 | 172 175 188 |
| --dw-primary | 83 35 181 | 183 163 255 |
| --dw-primary-foreground | 255 255 255 | 26 26 33 |
| --dw-border | 229 231 235 | 80 82 97 |
| --dw-input | 209 213 219 | 116 119 136 |
| --dw-destructive | 220 38 38 | 254 139 139 |
| --dw-success | 22 128 90 | 70 205 138 |
| --dw-warning | 180 83 9 | 251 191 60 |
| --dw-shadow-color | 15 23 42 | 0 0 0 |
| --dw-overlay | rgb(15 23 42 / 0.55) | rgb(0 0 0 / 0.72) |
| --dw-color-scheme | light | dark |
Three of those are worth calling out. --dw-background steps up from the
near-black most dark apps use, because a widget cannot build an edge against
its own value — if your page and the widget share a background, no border and
no shadow will separate them. --dw-shadow-color goes to black, because the
light default (slate) is lighter than a dark page and would make every
--dw-elev-* step inert. And --dw-primary-foreground flips to dark ink; see
the invariant below for why it has to.
Your :root still wins. Both dark blocks live inside @layer theme, exactly
like the light defaults, so an unlayered host declaration beats them at any OS
setting, in any import order. Verified in Chromium:
| your setup | OS light | OS dark |
|---|---|---|
| no data-dw-theme | light | light |
| data-dw-theme="dark" | dark | dark |
| data-dw-theme="auto" | light | dark |
| full unlayered :root | yours | yours |
If you opt in with a partial palette, re-point the whole thing. Declaring
--dw-primary and letting the SDK supply the grounds is fine in light and breaks
in dark: a light-tuned mid-luminance violet as md-link ink on the dark
--dw-muted measures 1.16:1, and status icons land at ~2.0:1 on their own 12%
tints. CSS gives no way to ask "did the author declare this property", so the SDK
cannot detect a half-set palette and compensate. If you set data-dw-theme, own
every token in the table above or none of them.
One invariant you own the moment you re-point the brand: --dw-primary must
clear 4.5:1 against --dw-primary-foreground. This is the pair that breaks
first, because --dw-primary has two jobs: a solid fill under
--dw-primary-foreground (chat-send, form-submit, the customer's own chat
bubbles, the current journey marker) and ink on the page (md-link,
history-new). On a light ground one dark brand value serves both. On a dark
ground it cannot: a violet dark enough to carry white text is too dark to read
as a link on a dark surface, and lightening it for the link drops the filled
controls below 4.5:1. Tune the hue for ink and flip the foreground to dark —
that is what the shipped dark values do, and they measure 4.95:1 as md-link
ink on --dw-muted and 7.99:1 as a fill under dark ink. If you re-point
--dw-primary for dark, re-point --dw-primary-foreground with it.
--dw-color-scheme covers the native controls. Checkbox ticks, the date
input's calendar glyph, <select> arrows and the scroll regions are painted by
the browser and read none of the other --dw-* variables; color-scheme is the
only lever. The SDK declares it on [data-dw-part] from this token, so it
reaches every control inside the widget and nothing outside it — deliberately
not on :root, where it would repaint your page's canvas and document
scrollbars. Measured on an unchecked checkbox: rgb(59 59 59) under
--dw-color-scheme: dark, rgb(255 255 255) under light.
Focus ring
Drawn once for every interactive part by a @layer components rule, so there is
no per-component ring to chase.
| variable | default |
|---|---|
| --dw-ring | var(--dw-primary) (triplet — inherits your brand for free) |
| --dw-focus-width | 2px |
| --dw-focus-offset | 2px |
Elevation
--dw-shadow-color is the ink; the four steps are complete box-shadow
strings. Set a step to none to flatten that tier everywhere.
| variable | default | used for |
|---|---|---|
| --dw-shadow-color | 15 23 42 | shadow ink (triplet) |
| --dw-elev-1 | 0 1px 2px …/.06 | chips, badges, widget root, project card |
| --dw-elev-2 | 0 1px 2px …/.05, 0 2px 6px -2px …/.08 | cards, task groups, form card |
| --dw-elev-3 | 0 2px 4px …/.05, 0 10px 24px -8px …/.16 | toast |
| --dw-elev-4 | 0 2px 4px …/.06, 0 24px 48px -16px …/.28 | drawer, lightbox |
Overlay
The modal scrim. Full color values, not triplets — the scrim carries its own alpha.
| variable | default |
|---|---|
| --dw-overlay | rgb(15 23 42 / 0.55) |
| --dw-overlay-blur | 2px |
Radius
| variable | default | used for |
|---|---|---|
| --dw-radius-sm | 0.375rem | chips, inline code, thumbnails, bubble tail |
| --dw-radius-md | 0.5rem | inputs, buttons, rows, lightbox panel |
| --dw-radius-lg | 0.75rem | cards, bubbles, toast, task groups |
| --dw-radius-xl | 1rem | widget root, task drawer |
There is no --dw-radius-full; pills, dots and avatars are fully round by
design and are not on the radius scale.
Motion
| variable | default |
|---|---|
| --dw-default-transition-duration | 160ms |
| --dw-default-transition-timing-function | cubic-bezier(0.2, 0.7, 0.2, 1) |
| --dw-animate-spin / --dw-animate-pulse / --dw-animate-bounce | Tailwind defaults |
Under prefers-reduced-motion: reduce the SDK already sets the three
--dw-animate-* tokens to none and the transition duration to 0ms, for the
whole widget at once. You do not need to add your own guard.
Zeroing those tokens is the graceful half. It cannot be the whole guard, because
--dw-default-transition-duration is a dial you are invited to set: a host
that declares :root { --dw-default-transition-duration: 150ms } in its own
unlayered sheet wins that variable in every media context, reduced-motion
included, and the widget animates anyway. So a second, token-independent floor
(see the !important note above) backs it up. Net effect: set the duration
token to whatever you like — reduced-motion users still get a still interface,
and you do not have to remember to write the media query yourself.
2. Target one element
Every styled element carries a data-dw-part attribute naming its role in the
anatomy. Select it directly — an attribute selector has the same specificity as
a class, and the same unlayered-wins rule applies.
[data-dw-part='task-row'] { border-radius: 0; }Some parts also carry state attributes you can key off:
data-dw-role (user|agent), data-dw-state (loading|error|empty|ready),
data-dw-status (slugified status value — see below), data-dw-step (done|current|upcoming),
data-dw-selected, data-dw-unread, data-dw-busy, data-dw-level (1–6),
data-dw-tool-state, data-dw-held, data-dw-field, data-dw-type, data-dw-empty.
[data-dw-part='chat-bubble'][data-dw-role='user'] { background: #0d9488; }
[data-dw-part='journey-marker'][data-dw-step='done'] { background: #ecfdf5; }data-dw-status carries your own status value, lowercased with every run of
separators collapsed to a single -. Letters and digits survive in any
script, so a non-Latin status keeps its identity and you quote it like any
other value — In Progress → in-progress, 進行中 → 進行中, Готово →
готово:
[data-dw-part='task-row'][data-dw-status='進行中'] { background: #fffbeb; }Documented-stable part names
The 74 names below are the public contract and will not be renamed without a
major version. (That count is just the length of this list — the SDK ships more
data-dw-part values than these; the rest exist in the DOM but are present,
not yet guaranteed, so style them at your own risk.)
attachment-chip · attachment-list · auth-card · auth-input ·
auth-submit · chat-bubble · chat-composer · chat-error · chat-header ·
chat-input · chat-name · chat-pane · chat-root · chat-row ·
chat-send · chat-toast · chat-tool-chip · comment · comment-body ·
comment-composer · comment-input · comment-submit · comments ·
comments-list · field · field-input · field-label · field-value ·
form-card · form-submit · history-list · history-root · history-row ·
journey · journey-marker · journey-step · journey-title ·
lightbox-overlay · lightbox-panel · md-blockquote · md-code-inline ·
md-heading · md-link · md-list · md-p · md-pre · md-root ·
md-table · messages · picker-button · picker-chip · progress ·
progress-fill · progress-track · progress-value · project-card ·
project-error · project-header · project-title · status-badge ·
status-dot · status-notice · status-select · status-select-control ·
task-detail-backdrop · task-detail-body · task-detail-panel ·
task-detail-save · task-detail-title · task-group · task-group-header ·
task-list · task-row · task-row-title
One caveat on that list: chat-tool-chip is unreachable in the current
build. MessageBubble renders it (and its chat-tool-chips wrapper) from
ChatMessage.toolCalls, but nothing in the SDK ever populates that field, so no
render path produces the element. The name and its data-dw-tool-state values
are held for when the tool-call surface is wired back up; a selector you write
against it today is valid CSS that simply never matches. Every other name above
is live in the DOM and covered by a test.
fill, sidekick, sidekick-launcher, sidekick-badge and sidekick-panel
are reserved for host portal shells — the SDK will never mint them.
Examples
Recolor to your brand. Two variables move the majority of the surface;
--dw-ring follows --dw-primary automatically.
:root {
--dw-primary: 13 148 136;
--dw-primary-foreground: 255 255 255;
}Re-radius everything. Four variables, one step each — every card, bubble, input and chip follows.
:root {
--dw-radius-sm: 2px;
--dw-radius-md: 3px;
--dw-radius-lg: 4px;
--dw-radius-xl: 6px;
}Go square and flat. The radius and elevation axes both run to zero, and
between them they carry the whole "soft product card" vs. "technical blueprint"
distinction — no part selectors needed. Radius 0 squares every corner that is
on the scale (pills, dots and avatars stay round by design — see above); each
elevation step set to none removes that tier's shadow entirely, leaving the
1px hairlines as the only separation.
:root {
--dw-radius-sm: 0;
--dw-radius-md: 0;
--dw-radius-lg: 0;
--dw-radius-xl: 0;
--dw-elev-1: none;
--dw-elev-2: none;
--dw-elev-3: none;
--dw-elev-4: none;
}Flatten only the upper tiers (--dw-elev-3/-4: none) if you want cards to
keep their lift but modals to sit flush.
Restyle one element. No variable covers "make task rows a card"; reach for the part instead.
[data-dw-part='task-row'] {
margin-block: 4px;
border-top: 0;
border-radius: var(--dw-radius-md);
box-shadow: var(--dw-elev-1);
}Two caveats
Overriding
backgroundorcoloron a part also kills that element's:hoverand:focus-visiblefill. Your unlayered rule beats the SDK's layered utility in every state, not just the resting one, so re-declare the states you still want:[data-dw-part='task-row'] { background: #fafafa; } [data-dw-part='task-row']:hover { background: #f0f0f0; }Overriding
border-radiusorbox-shadowhas no such side effect — nothing changes those on hover.Two radii are not tokenised. A handful of literals sit outside the scale: bare
roundedcompiles to a hardcoded0.25rem, androunded-fullcompiles tocalc(infinity * 1px)with no--dw-radius-fullbehind it. If you need to change a pill or a dot, target its part.
Documentation
Full guide in the Statisfy help center:
- Overview — architecture and the auth model
- Getting Started — token,
StatisfyProvider, first component - Digital Worker Chat — chat components, inline forms, headless client
- Project Module —
ProjectModule,useProject, primitives - API Reference — endpoints, headers, error codes, types
License
MIT
