惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
U
Unit 42
N
Netflix TechBlog - Medium
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
小众软件
小众软件
V
Visual Studio Blog
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
博客园 - 叶小钗
GbyAI
GbyAI
爱范儿
爱范儿
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
D
DataBreaches.Net
博客园_首页
D
Docker
A
About on SuperTechFans
G
Google Developers Blog
I
InfoQ
T
The Blog of Author Tim Ferriss
V
V2EX
博客园 - Franky

Chat SDK Documentation

History | Chat SDK History | Chat SDK List a vendor-official adapter | Chat SDK Approvals | Chat SDK Vercel Connect | Chat SDK Teams Low-Level APIs | Chat SDK CLI | Chat SDK Platform Adapters | Chat SDK State Adapters | Chat SDK Cards | Chat SDK Getting Started | Chat SDK Introduction | Chat SDK Slack Low-Level APIs | Chat SDK Streaming | Chat SDK Testing | Chat SDK Overview | Chat SDK toAiMessages | Chat SDK Cards | Chat SDK Overview | Chat SDK Markdown | Chat SDK Modals | Chat SDK AI SDK Tools | Chat SDK Types | Chat SDK Message Subject | Chat SDK Conversation History | Chat SDK Transcripts | Chat SDK Slack bot with Next.js and Redis Actions | Chat SDK Direct Messages | Chat SDK Emoji | Chat SDK
Modals | Chat SDK
Vercel · 2026-05-29 · via Chat SDK Documentation

Collect structured user input through modal dialogs with text fields, dropdowns, and validation.

Modals open form dialogs in response to button clicks or slash commands. They support text inputs, dropdowns, radio buttons, and server-side validation. Currently supported on Slack and Teams.

Modals are opened from action handlers or slash command handlers using event.openModal():

import { Modal, TextInput, Select, SelectOption } from "chat";

bot.onAction("feedback", async (event) => {
  await event.openModal(
    <Modal
      callbackId="feedback_form"
      title="Send Feedback"
      submitLabel="Send"
      closeLabel="Cancel"
      notifyOnClose
    >
      <TextInput
        id="message"
        label="Your Feedback"
        placeholder="Tell us what you think..."
        multiline
      />
      <Select id="category" label="Category" placeholder="Select a category">
        <SelectOption label="Bug Report" value="bug" />
        <SelectOption label="Feature Request" value="feature" />
        <SelectOption label="General" value="general" />
      </Select>
      <TextInput
        id="email"
        label="Email (optional)"
        placeholder="your@email.com"
        optional
      />
    </Modal>
  );
});

The top-level container for the form.

PropTypeDescription
callbackIdstringIdentifier for matching submit/close handlers
titlestringModal title
submitLabelstring (optional)Submit button text (defaults to "Submit")
closeLabelstring (optional)Cancel button text (defaults to "Cancel")
notifyOnCloseboolean (optional)Fire onModalClose when user cancels
callbackUrlstring (optional)URL to POST form values to on submit
privateMetadatastring (optional)Custom context passed through to handlers

TextInput

A text field for user input.

PropTypeDescription
idstringField identifier (key in event.values)
labelstringField label
placeholderstring (optional)Placeholder text
initialValuestring (optional)Pre-filled value
multilineboolean (optional)Render as textarea
optionalboolean (optional)Allow empty submission
maxLengthnumber (optional)Maximum character count

Select

A dropdown for selecting a single option.

PropTypeDescription
idstringField identifier
labelstringField label
placeholderstring (optional)Placeholder text
initialOptionstring (optional)Pre-selected value
optionalboolean (optional)Allow empty submission

ExternalSelect

A dropdown that loads its options dynamically from a handler as the user types. Useful for large or remote-backed option sets (people, tickets, records) where a static <Select> would be impractical. Slack-only.

PropTypeDescription
idstringField identifier (key in event.values)
labelstringField label
placeholderstring (optional)Placeholder text
minQueryLengthnumber (optional)Minimum characters before the loader fires (Slack default: 3)
initialOption{ label, value } (optional)Pre-selected option when the modal opens (must match an option returned by the loader). For static <Select>, initialOption is just the value string — for <ExternalSelect> it's the full { label, value } object since the loader hasn't run yet.
optionalboolean (optional)Allow empty submission

Register the loader with onOptionsLoad:

import { ExternalSelect, Modal } from "chat";

bot.onAction("assign", async (event) => {
  await event.openModal(
    <Modal callbackId="assign_form" title="Assign to…">
      <ExternalSelect
        id="assignee"
        label="Assignee"
        placeholder="Search people"
        minQueryLength={1}
      />
    </Modal>
  );
});

bot.onOptionsLoad("assignee", async (event) => {
  const people = await peopleService.search(event.query);
  return people.map((p) => ({ label: p.fullName, value: p.id }));
});

bot.onModalSubmit("assign_form", async (event) => {
  const assigneeId = event.values.assignee;
  // …
});

The selected value arrives in event.values on submit just like a static <Select>.

Grouped options

Return an array of groups instead of a flat options array to render headers between sections (e.g. "Recent" / "All"):

bot.onOptionsLoad("assignee", async (event) => {
  const [recent, all] = await Promise.all([
    peopleService.recent(event.user.userId),
    peopleService.search(event.query),
  ]);
  return [
    { label: "Recent", options: recent.map((p) => ({ label: p.fullName, value: p.id })) },
    { label: "All", options: all.map((p) => ({ label: p.fullName, value: p.id })) },
  ];
});

Slack limits: max 100 groups, max 100 options per group, group label max 75 characters.

Slack requires a response within 3 seconds for options requests. The adapter caps the loader at ~2.5s and returns an empty result on timeout — keep your loader fast (cache, prefetch, or narrow the query server-side).

Slack setup: ExternalSelect uses Slack's block_suggestion payload, which is dispatched to the Options Load URL. In your Slack app settings go to Interactivity & ShortcutsSelect Menus and set the Options Load URL to the same endpoint as your Interactivity Request URL (e.g. https://your-domain.com/api/webhooks/slack). Without this, typing into an external select will silently return no results.

RadioSelect

A radio button group for mutually exclusive options.

PropTypeDescription
idstringField identifier
labelstringField label
initialOptionstring (optional)Pre-selected value
optionalboolean (optional)Allow empty submission

SelectOption

An option for Select or RadioSelect.

PropTypeDescription
labelstringDisplay text
valuestringValue passed to handler
descriptionstring (optional)Help text below the label

Register a handler with onModalSubmit using the same callbackId:

bot.onModalSubmit("feedback_form", async (event) => {
  const { message, category, email } = event.values;

  // Validate input — return errors to show in the modal
  if (!message || message.length < 5) {
    return {
      action: "errors",
      errors: { message: "Feedback must be at least 5 characters" },
    };
  }

  // Post confirmation to the original thread
  if (event.relatedThread) {
    await event.relatedThread.post(`Feedback received! Category: ${category}`);
  }

  // Update the message that triggered the modal
  if (event.relatedMessage) {
    await event.relatedMessage.edit("Feedback submitted!");
  }

  // Return nothing (or { action: "close" }) to close the modal
});

Response types

ResponseDescription
undefined or { action: "close" }Close the current view (goes back one level in the stack)
{ action: "clear" }Close all views and dismiss the modal entirely
{ action: "errors", errors: { fieldId: "message" } }Show validation errors on specific fields
{ action: "update", modal: ModalElement }Replace the modal content
{ action: "push", modal: ModalElement }Push a new modal view onto the stack

ModalSubmitEvent

PropertyTypeDescription
callbackIdstringModal identifier
viewIdstringPlatform view ID
valuesRecord<string, string>Form field values keyed by input id
userAuthorThe user who submitted
privateMetadatastring (optional)Custom context from the Modal component
relatedThreadThread (optional)Thread where the modal was triggered
relatedMessageSentMessage (optional)Message with the button that opened the modal
relatedChannelChannel (optional)Channel where the modal was triggered (from slash commands)
adapterAdapterThe platform adapter
rawunknownPlatform-specific payload

Optionally handle when users cancel a modal. Requires notifyOnClose on the Modal component:

bot.onModalClose("feedback_form", async (event) => {
  console.log(`${event.user.userName} cancelled the feedback form`);

  if (event.relatedThread) {
    await event.relatedThread.post("No worries, let us know if you change your mind!");
  }
});

Like buttons, modals accept a callbackUrl. When the modal is submitted, the form values are POSTed to the URL:

await event.openModal(
  <Modal callbackUrl={webhook.url} callbackId="intake" title="Request Access" submitLabel="Submit">
    <TextInput id="reason" label="Reason" multiline />
  </Modal>
);

The POST body for modal submissions:

{
  "type": "modal_submit",
  "callbackId": "intake",
  "values": { "reason": "Need access to production logs" },
  "user": { "id": "U123", "name": "alice" }
}

Use privateMetadata to carry context from the button click through to the submit handler:

bot.onAction("report", async (event) => {
  await event.openModal(
    <Modal
      callbackId="report_form"
      title="Report Bug"
      submitLabel="Submit"
      privateMetadata={JSON.stringify({
        reportType: event.value,
        threadId: event.threadId,
      })}
    >
      <TextInput id="title" label="Bug Title" />
      <TextInput id="steps" label="Steps to Reproduce" multiline />
    </Modal>
  );
});

bot.onModalSubmit("report_form", async (event) => {
  const metadata = event.privateMetadata
    ? JSON.parse(event.privateMetadata)
    : {};

  console.log(metadata.reportType); // "bug"
});