Web SDK response components
How the Runbear chat widget renders the confirm, select, card, list, table and chart components an agent attaches to its reply, what a click sends, and how components reach you on the API.
On this page
Alongside its prose, an agent can attach a response component: a small block the widget renders
directly under the reply. The built-in components are confirm, a short yes/no style question;
select, a set of choices; card, a formatted block of details about one record, with an optional
image and links; list, a set of search results or records with links, thumbnails and short
facts; table, rows of records the visitor can sort by column and page through; and chart, a
line, bar or area chart of numbers.
confirm and select are offers the visitor answers. The display components — card,
list, table and chart — have nothing to answer: the agent shows them without pausing for a
reply, so they can arrive while the rest of the answer is still being written. The visitor can page
through a list or a table, sort a table by a column, hover over or focus a chart point for its
values, narrow a date chart's range, or view a chart as a table, right in the widget, without
sending anything or spending a turn.
This page is the rendering side. Whether and how often an agent produces components is per-agent: the Component frequency select and the per-component switches under Agent settings → Response components, or the same fields on the Response Components API. New components start off. Turning the agent's Choice buttons (all channels) switch off withholds every component, and scheduled or automated runs never offer one.
What you need#
- Response components enabled for your organization (see the callout above).
- An agent whose Component frequency is set above
off, with the component's switch on. - An SDK release that renders the component; see which SDK release renders what.
Which SDK release renders what#
| Component | Renders from | On an older release the visitor sees |
|---|---|---|
confirm | 0.3.0 | 0.2.x and earlier: the reply's text with the choices appended as a plain list |
select | 0.3.0 | Same as confirm |
card | 0.4.0 | 0.3.x: the card's plain-text summary (fallbackText). 0.2.x and earlier: that summary appended to the reply's text |
card with image or links | 0.6.0 | 0.4.x and 0.5.x: the card without its image and links. Anything the links carry must also be in the card's text |
list | 0.6.0 | 0.3.x to 0.5.x: its fallbackText. 0.2.x and earlier: that summary appended to the reply's text |
table | 0.6.0 | Same as list |
chart | 0.6.0 | Same as list. The summary states the key numbers |
This is the intended forward-compatibility behavior, not a defect: an older widget never breaks on a newer component, it shows the summary instead. Upgrade the widget before switching a component on in Web SDK components.
From 0.6.0 a widget can also opt out of rendering a component: config.components.disabled shows
each component it names as its fallbackText instead (see
Configuration). It takes component names such as "chart",
not the agent's switch names such as choiceButtons, and it only changes what this widget renders —
the agent still produces the component. To stop the agent producing it, turn its switch off.
A click is a message#
This section is about confirm and select; a display component (card, list, table, chart)
carries no options and never sends anything. Opening one of their links is ordinary navigation, not a message.
Activating an option sends that option's value into the conversation exactly as if the visitor had typed it. There is no separate callback and no component-level handler, and no answer state for you to read or reconcile: the transcript is the record. Three things worth planning for:
- A click costs a turn. In session mode a pass carries a lifetime turn cap, 100 by default, which never resets for the life of that session. A click spends one exactly as a typed message would: the click is the visitor's next message, not an extra one on top of it, so buttons don't consume the cap any faster than typing does.
- The conversation never pauses. A component is an offer, not a modal: nothing waits on a click, and answering in your own words instead is always valid. The message box behaves exactly as it does on any other turn.
- One activation per open question. The widget drops a second click on a component that has already been used, so a double-click can't bill two turns. Each activation that does go through carries its own id, so two genuine answers aren't collapsed into one.
The visitor can see what a click will send. When an option's value differs from its label, the
widget lists each such option under the buttons as a "Label → value" line, so a button reading
"Approve" that sends "Approve the request" says so before it is clicked.
From 0.6.0, a turn that renders a confirm or select, or shows any component as its plain-text
fallbackText, shows no suggestion chips, even with config.suggestions enabled: the chips
would repeat the component's own options, and clicking one would send a message that makes the real
component inert. A turn whose only components are display components (card, list, table,
chart) keeps its chips. On 0.3.x to 0.5.x every rendered component suppresses them.
A component goes inert once it is answered#
Interactive components only; a display component (card, list, table, chart) has no answered state. Paging, sorting and chart controls stay usable.
A component stays interactive while no visitor message appears after the message that carried it
and the widget is idle. Once a later visitor message exists, whether it came from a click, from
typing, or from your own addUserMessage call, the controls stop sending.
Inert controls stay on screen, readable and focusable. They are the record of what was offered, so a scrolled-back conversation still reads correctly. A group the visitor clicked names the option they picked; one that went inert some other way says why (answered, or a reply still arriving). All three strings are overridable. To style the two states, see Theming.
An unrecognized component renders prose#
Every component carries a fallbackText string. A client that reads components but can't render
this one shows that text instead of the controls: a name its renderer table predates, a known name
whose props fail that renderer's shape check, or a renderer that threw, which also logs
console.warn("Runbear: a message component failed to render"). On a reloaded transcript, a known
name with unreadable props is dropped instead, leaving the message's own text to carry it. This
is the forward-compatibility contract: a name added after your last upgrade reads as prose rather
than breaking the widget.
One shape check is worth knowing in advance: a chart shows its fallbackText when any of its
values is neither 0 nor between 1e-12 and 1e15 in magnitude, so a chart of tiny or
astronomical numbers reads as its summary.
A client that doesn't read components at all, such as an SDK from before this feature or your
own code against the REST API, is covered by a different mechanism: a prose rendering of the
component (a display component's fallbackText, or a prompt's choices) is appended to the end of the assistant
message's own content. It isn't byte-identical to what the component itself carries: markdown
metacharacters are escaped on the way in, and a prompt's block is built from its captions. Each
component appends its own paragraph, so a message carrying four cards ends with four of them. A
client that does render the components should suppress those trailing blocks by position, one per
component it rendered, rather than by matching their text.
Surviving a tab close#
Components are part of the conversation, so they come back with it. Whether the conversation itself
comes back is decided by auth.storage, which by default discards it when the tab closes. See
Where the session is stored. Components vanishing
when the tab closes is that default working, not a defect.
Customizing the rendered strings#
Every user-visible string the widget adds to a component can be replaced: the confirm and
select captions, and from 0.6.0 the pager, table and chart strings. The rest of what a component
shows is the agent's own text. Each field is optional and is defaulted where it is shown, so a
partial override replaces only what it names.
const client = new Runbear({
assistantId: "…",
config: {
components: {
labels: {
untitledGroup: "Choose an option",
optionCount: "{count} options available",
answered: "Answered.",
chosen: "You chose: {label}",
streaming: "Waiting for the assistant…",
},
},
},
})| Label | Default | Shown when | Since |
|---|---|---|---|
untitledGroup | "Choose an option" | The accessible name for a group whose reply carried no text to borrow | 0.3.0 |
optionCount | "{count} options available" | Announced once per group by a screen reader | 0.3.0 |
answered | "Answered." | The group is inert because a later message answered it and the visitor didn't click it on this page: a reloaded transcript, or a second group in the same turn | 0.3.0 |
chosen | "You chose: {label}" | The visitor clicked one of this group's options on this page. Shown from the click onward and outranks answered, so write it as a record ("You chose: X"), not as a transient "Sending…" | 0.3.0 |
streaming | "Waiting for the assistant…" | The group is inert because the assistant is still replying | 0.3.0 |
pagerPrevious | "Previous" | The button that shows the previous page of a list or table | 0.6.0 |
pagerNext | "Next" | The button that shows the next page of a list or table | 0.6.0 |
pagerStatus | "Page {page} of {count}" | The page status a screen reader announces | 0.6.0 |
tableUntitled | "Table with {count} rows" | The accessible caption of a table with no title | 0.6.0 |
tableSortedAscending | "Sorted by {label}, ascending" | Announced after a table is sorted ascending by a column | 0.6.0 |
tableSortedDescending | "Sorted by {label}, descending" | Announced after a table is sorted descending by a column | 0.6.0 |
tableUnsorted | "Original order" | Announced when a table returns to its original row order | 0.6.0 |
chartTypeLine | "Line chart" | The start of a line chart's accessible name | 0.6.0 |
chartTypeBar | "Bar chart" | The start of a bar chart's accessible name | 0.6.0 |
chartTypeArea | "Area chart" | The start of an area chart's accessible name | 0.6.0 |
chartSummary | "{count} series, {from} to {to}, values from {min} to {max}." | The chart's screen-reader summary after its type and title; {count} is the series, {from} and {to} the first and last x, {min} and {max} the smallest and largest value | 0.6.0 |
chartHiddenSeries | "…and {count} more series" | Under a chart that carried more series than it draws | 0.6.0 |
The placeholders in braces are replaced where they appear. An older SDK ignores a key it does not know.
Components on the API#
If you read the API directly rather than through the SDK, components reach you on two carriers, both carrying the same object.
On the thread listing. GET /v1/threads/{threadId}/messages adds an optional components array
to each message, in the order the components were produced, omitted entirely when the message
carried none. At most 4 per message, counting every kind together.
On the stream. Both streaming routes, POST /v1/threads/{threadId}/runs/stream and the
streaming form of POST /v1/chat/completions, publish a thread.message.component event (see
Streaming). It carries no message id, and its position relative to
thread.message.completed is not guaranteed: a confirm or select follows the completed reply
it decorates, while a display component (card, list, table or chart) can be published mid-turn, before it. Attach the component to the
assistant turn you are currently rendering rather than to the last message you saw complete.
{"event":"thread.message.component","data":{"componentId":"9f2b…","name":"confirm","props":{"options":[{"label":"Approve","value":"Approve the request","style":"primary"},{"label":"Reject","value":"Reject the request","style":"danger"}]},"fallbackText":"You can reply with:\n• Approve\n• Reject"}}| Field | Type | Notes |
|---|---|---|
componentId | string | Opaque key for this instance, echoed back for analytics only. It is not an authorization input, and the server neither resolves it nor trusts it. |
name | string | The render key. An open vocabulary, see below. |
props | object | Payload whose shape depends on name, passed through verbatim. Built-in shapes: confirm and select carry an options array of { label, value, style? }, with the question itself in the message's content; an option's style is default (outline), primary or danger (destructive), and any other token renders as default; card carries a title, an optional subtitle, an optional fields array of { label, value }, an optional image of { url, alt }, and an optional links array of up to 3 { label, url }. The SDK shows at most 10 fields and cuts a title past 200 characters, a subtitle past 500, a field label past 75 and a field value past 300 with "…"; fields beyond the tenth collapse into one "…and N more" line. A card without a title shows its fallbackText. list carries an optional title and an items array of 1 to 10 { title, subtitle?, url?, image?, meta? }, where image is { url, alt } and meta is up to 3 { label, value }; the SDK shows 5 items per page with Previous and Next controls. Every URL in a card or a list is https. table carries an optional title of up to 200 characters, a columns array of 1 to 8 { label, align? } with labels of up to 60 characters, and a rows array of 1 to 30 { cells }, where cells holds exactly one string of up to 80 characters per column; align is left or right, and any other token renders as left. The SDK drops a row whose cell count does not match the columns, sorts by any column and shows 10 rows per page. chart carries a type (line, bar or area), an optional title, an optional xType (category, the default, or date, where every x value is a YYYY-MM-DD date), an x array of 2 to 100 labels, an optional xLabel, yLabel and yUnit, and a series array of 1 to 4 { name, values }, where values holds one finite number per entry of x; series colours come from the widget's theme, never from props. A client that does not recognise name must not read props. |
fallbackText | string | Prose to show whenever no renderer matches name. Never empty. |
The SDK exports ChatComponent for this object, and ConfirmProps and SelectProps for narrowing
props once you have checked name. CardProps is exported from 0.5.0, and gains image and
links in 0.6.0. See Types.
Server limits#
Runbear holds every component to these bounds before it is published:
| What | Limit |
|---|---|
| Components per message | At most 4, counting every kind together |
confirm and select options | 1 to 10 per component |
Option label | At most 75 characters |
Option value | At most 200 characters |
fallbackText | At most 2,000 characters |
name | Lowercase letters, digits and hyphens, starting with a letter (^[a-z][a-z0-9-]*$), at most 64 characters. constructor is reserved and rejected |
componentId | At most 64 characters |
props | At most 65,536 characters of JSON |
Posting a click yourself#
To post a click from your own client, send the option's value as an ordinary message. Give the
message a distinct id if you send two identical values in one conversation, or the second
collapses into the first and the agent answers the first choice. A message id is 1–200 characters. Don't
start one with rbc:: that prefix is reserved for the SDK's own click ids, whose shape is
rbc:<uuid>:<uuid>, and anything else behind it rejects the whole run with a 400.
Related#
- Response Components API: read and set the per-agent Component frequency and component switches over the agents API
- Response components settings: the same dial and switches in the dashboard
- Theming: the component class and data hooks
- Configuration:
config.components.disabled