# Web SDK response components

> How the Runbear chat widget renders the confirm, select and card components an agent attaches to its reply, what a click sends, and how components reach you on the API.

Source: https://docs.runbear.io/api/web-sdk/response-components

Last updated: 2026-09-30

> **Note**
>
> **Limited availability.** This feature is being rolled out gradually. Contact [support@runbear.io](mailto:support@runbear.io) to enable it for your organization.

Alongside its prose, an agent can attach a **response component**: a small block the widget renders
directly under the reply. v1 ships three: `confirm`, a short yes/no style question, `select`, a list
of choices, and `card`, a formatted block of details.

The first two are offers the visitor answers. A card has nothing to activate: the agent shows it
without pausing for a reply, so a card can arrive while the rest of the answer is still being
written.

This page is the rendering side. Whether and how often an agent produces components is a per-agent
setting: the **Component frequency** select under **Agent settings →
[Choice buttons](/agents/settings.md#choice-buttons)**, or the same field on the
[Response Components API](/api/response-components.md). Turning choice buttons off withholds every
component, the card included, 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`.
- An SDK release that carries the renderers: **`@runbear-io/react` 0.3.0 or newer** for `confirm`
  and `select`, and **0.4.0 or newer** for `card`.

Under the floor for a given component the visitor reads it as prose instead of seeing it rendered:
0.2.x and earlier don't read components at all and show only the prose the message text carries,
while 0.3.x renders a card's `fallbackText` in place of the formatted block but handles `confirm`
and `select` normally. That is the intended behavior, not a defect.

## A click is a message

This section is about `confirm` and `select`; a card carries no options and never sends anything.

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.

A turn that renders a component 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 component goes inert once it is answered

Interactive components only; a card has no controls and no answered state.

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](/api/web-sdk/theming.md#class-and-data-hooks).

## 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.

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 card'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](/api/web-sdk/authentication.md#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 built-in `confirm` and `select` components render can be replaced; a
card shows only 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.

```ts
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           | Shown when                                                                                                                                                                                 | Placeholder |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `untitledGroup` | The accessible name for a group whose reply carried no text to borrow                                                                                                                      | —           |
| `optionCount`   | Announced once per group by a screen reader                                                                                                                                                | `{count}`   |
| `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                         | —           |
| `chosen`        | 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…" | `{label}`   |
| `streaming`     | The group is inert because the assistant is still replying                                                                                                                                 | —           |

The values shown in the example are the defaults.

## 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.

**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](/api/streaming.md)). 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 `card` 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.

```json
{"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`, and an optional `fields` array of `{ label, value }`. 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`. A client that does not recognise `name` must not read `props`. |
| `fallbackText` | string | Prose to show whenever no renderer matches `name`. Never empty.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

> **name is an open vocabulary, not an enum**
>
> Built-in names include `confirm`, `select` and `card`. **Treat any other value as a component this
> client does not render and show `fallbackText`.** Don't generate a closed union from this list: the
> first name appended would break every client that had not regenerated. The same applies to an
> option's `style`: an unrecognized token must degrade to the default.

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 (not yet released). See [Types](/api/web-sdk/types.md#response-components).

### Server limits

Runbear holds every component to these bounds before it is published:

| What                           | Limit                                                                                                                                              |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Components per message         | At most 4                                                                                                                                          |
| `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 32,768 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](/api/response-components.md): read and set the per-agent **Component
  frequency** over the agents API
- [Choice buttons](/agents/settings.md#choice-buttons): the same dial in the dashboard
- [Theming](/api/web-sdk/theming.md#class-and-data-hooks): the component class and data hooks
- [Configuration](/api/web-sdk/configuration.md#component-labels): the label defaults
