Looking for the chatbot template? It's now here.

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():

lib/bot.tsx
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

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

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

Handle submissions

Register a handler with onModalSubmit using the same callbackId:

lib/bot.ts
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 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

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

Handle cancellation

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

lib/bot.ts
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:

lib/bot.tsx
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"
});