Modals
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.
Open a modal
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>
);
});Components
Modal
The top-level container for the form.
| Prop | Type | Description |
|---|---|---|
callbackId | string | Identifier for matching submit/close handlers |
title | string | Modal title |
submitLabel | string (optional) | Submit button text (defaults to "Submit") |
closeLabel | string (optional) | Cancel button text (defaults to "Cancel") |
notifyOnClose | boolean (optional) | Fire onModalClose when user cancels |
privateMetadata | string (optional) | Custom context passed through to handlers |
TextInput
A text field for user input.
| Prop | Type | Description |
|---|---|---|
id | string | Field identifier (key in event.values) |
label | string | Field label |
placeholder | string (optional) | Placeholder text |
initialValue | string (optional) | Pre-filled value |
multiline | boolean (optional) | Render as textarea |
optional | boolean (optional) | Allow empty submission |
maxLength | number (optional) | Maximum character count |
Select
A dropdown for selecting a single option.
| Prop | Type | Description |
|---|---|---|
id | string | Field identifier |
label | string | Field label |
placeholder | string (optional) | Placeholder text |
initialOption | string (optional) | Pre-selected value |
optional | boolean (optional) | Allow empty submission |
RadioSelect
A radio button group for mutually exclusive options.
| Prop | Type | Description |
|---|---|---|
id | string | Field identifier |
label | string | Field label |
initialOption | string (optional) | Pre-selected value |
optional | boolean (optional) | Allow empty submission |
SelectOption
An option for Select or RadioSelect.
| Prop | Type | Description |
|---|---|---|
label | string | Display text |
value | string | Value passed to handler |
description | string (optional) | Help text below the label |
Handle submissions
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
| Response | Description |
|---|---|
undefined or { action: "close" } | Close the modal |
{ 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
| Property | Type | Description |
|---|---|---|
callbackId | string | Modal identifier |
viewId | string | Platform view ID |
values | Record<string, string> | Form field values keyed by input id |
user | Author | The user who submitted |
privateMetadata | string (optional) | Custom context from the Modal component |
relatedThread | Thread (optional) | Thread where the modal was triggered |
relatedMessage | SentMessage (optional) | Message with the button that opened the modal |
relatedChannel | Channel (optional) | Channel where the modal was triggered (from slash commands) |
adapter | Adapter | The platform adapter |
raw | unknown | Platform-specific payload |
Handle cancellation
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!");
}
});Pass context with privateMetadata
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"
});