JSX Callbacks

The handlers a component can carry, what they receive, and how the click resolves.


Interactive components carry a callback. When the user clicks a button, picks from a select, submits an input, or reacts to a message, your handler runs with the thread and the interaction in hand.

This is the reference. For the walkthrough, see the UI library.

Which component takes which callback#

ComponentPropHandler typeFires when
ButtononClickClickHandler<TValue>the button is clicked
SelectonSelectClickHandler<string | string[]>an option is chosen (multi gives an array)
InputonSubmitClickHandler<string>the input is submitted
MessageonReactionMessageReactionHandlera user reacts to the message

ClickHandler#

onClick, onSelect, and onSubmit are all a ClickHandler. It receives one InteractionContext:

type ClickHandler<TValue = unknown> = (ctx: InteractionContext<TValue>) => void | Promise<void>;
FieldTypeDescription
threadThreadThe conversation. Post, update, run the agent, block on a choice. See the Thread API.
messageIncomingMessageThe message the control lives on. message.ref updates that message in place.
action.idstringThe control's opaque id.
action.valueTValueThe value the control carried, typed by TValue.
valuesRecord<string, unknown>All input values submitted with this interaction.
userPlatformUserWho interacted.
platformstringThe platform the interaction came from.
openModal(view: ModalView) => Promise<{ ok, error? }>Open a modal. Capability-gated. On Discord, call it before any long work (the trigger expires in ~3s).

The value round-trip#

A control's value prop is echoed back on action.value, typed by TValue. That is how you tell clicks apart without separate ids:

<Button
  value={{ choice: "approve" }}
  onClick={async ({ thread, message, action }) => {
    action.value; // { choice: "approve" }
    await thread.update(message.ref, "Approved.");
  }}
>
  Approve
</Button>

Button, Select, Input#

import { Actions, Button, Select, Input } from "@copilotkit/channels/ui";

<Actions>
  <Button
    value={{ id: 42 }}
    style="primary"
    onClick={async ({ thread, action }) => {
      await thread.post(`Picked ${action.value.id}`);
    }}
  >
    Choose
  </Button>

  <Select
    placeholder="Priority"
    options={[
      { label: "High", value: "high" },
      { label: "Low", value: "low" },
    ]}
    onSelect={async ({ thread, action }) => {
      await thread.post(`Priority: ${action.value}`);
    }}
  />

  <Input
    placeholder="Add a note"
    onSubmit={async ({ thread, action }) => {
      await thread.post(`Note: ${action.value}`);
    }}
  />
</Actions>;

Button also takes url (a link button, which ignores onClick/value) and style: "primary" | "danger". Select takes multi for multi-select (where action.value is a string[]).

MessageReactionHandler#

<Message onReaction> is different: it takes the emoji first, then the full reaction.

type MessageReactionHandler = (emoji: EmojiValue, reaction: MessageReaction) => void | Promise<void>;
reaction fieldTypeDescription
emojiEmojiValueCanonical name when recognized, else the raw token. Same as the first argument.
rawEmojistringPlatform-native token.
addedbooleantrue = added, false = removed.
userPlatformUser?The reacting user, when reported.
messageIdstringId of the reacted message.
threadThreadPost back, run the agent, block on a choice, react.
messageRefMessageRefPass to thread.update(messageRef, ui) to redraw this message.
import { Message, Section } from "@copilotkit/channels/ui";

<Message
  onReaction={async (emoji, reaction) => {
    if (!reaction.added || emoji !== "thumbs_up") return;
    await reaction.thread.post(`Thanks, ${reaction.user?.name ?? "friend"}.`);
  }}
>
  <Section>{"React with a thumbs up."}</Section>
</Message>;

See reactions for more, and global reactions for reacting to any message rather than a specific card.

Handlers must survive a restart#

A click or reaction can arrive after your process restarts, when the in-memory handler is gone. Register every component that carries a callback in createChannel({ components }) so the handler is rebuilt from the durable snapshot. See the UI library.