Skip to main content
Modal components provide cross-platform dialog forms for collecting user input. They automatically convert to platform-specific formats.

Opening Modals

Modals are opened in response to user actions:
// From a button click (ActionEvent)
chat.onAction("feedback", async (event) => {
  await event.openModal(
    Modal({
      callbackId: "feedback_form",
      title: "Submit Feedback",
      children: [
        TextInput({ id: "message", label: "Your feedback" })
      ]
    })
  );
});

// From a slash command
chat.onSlashCommand("/feedback", async (event) => {
  await event.openModal(
    Modal({
      callbackId: "feedback_form",
      title: "Submit Feedback",
      children: [
        TextInput({ id: "message", label: "Your feedback" })
      ]
    })
  );
});

Handling Submissions

chat.onModalSubmit("feedback_form", async (event) => {
  const message = event.values.message;
  
  // Process the submission
  await saveFeedback(message);
  
  // Optionally return a response to update or close the modal
  return { action: "close" };
});
Root modal container.
callbackId
string
required
Unique identifier for routing submission events
title
string
required
Modal title displayed in header
children
ModalChild[]
Form input elements
submitLabel
string
Custom label for submit button (default: “Submit”)
closeLabel
string
Custom label for close/cancel button (default: “Cancel”)
notifyOnClose
boolean
If true, triggers onModalClose handler when user closes without submitting
privateMetadata
string
Arbitrary string passed through modal lifecycle (e.g., JSON context)
interface ModalOptions {
  callbackId: string;
  title: string;
  children?: ModalChild[];
  submitLabel?: string;
  closeLabel?: string;
  notifyOnClose?: boolean;
  privateMetadata?: string;
}

function Modal(options: ModalOptions): ModalElement;

ModalElement Type

interface ModalElement {
  type: "modal";
  callbackId: string;
  title: string;
  submitLabel?: string;
  closeLabel?: string;
  notifyOnClose?: boolean;
  privateMetadata?: string;
  children: ModalChild[];
}

type ModalChild =
  | TextInputElement
  | SelectElement
  | RadioSelectElement
  | TextElement
  | FieldsElement;

TextInput

Single or multi-line text input field.
id
string
required
Unique input ID (used to access value in submission)
label
string
required
Input label displayed above field
placeholder
string
Placeholder text shown when empty
initialValue
string
Pre-filled value
multiline
boolean
If true, renders as textarea
maxLength
number
Maximum character limit
optional
boolean
If true, field is not required for submission
interface TextInputOptions {
  id: string;
  label: string;
  placeholder?: string;
  initialValue?: string;
  multiline?: boolean;
  maxLength?: number;
  optional?: boolean;
}

function TextInput(options: TextInputOptions): TextInputElement;
Example:
TextInput({
  id: "email",
  label: "Email Address",
  placeholder: "[email protected]"
})

TextInput({
  id: "feedback",
  label: "Your Feedback",
  multiline: true,
  maxLength: 500,
  optional: true
})

Select

Dropdown select menu.
id
string
required
Unique input ID
label
string
required
Select label
options
SelectOptionElement[]
required
Available options
placeholder
string
Placeholder text shown when no option selected
initialOption
string
Pre-selected option value
optional
boolean
If true, field is not required for submission
interface SelectOptions {
  id: string;
  label: string;
  options: SelectOptionElement[];
  placeholder?: string;
  initialOption?: string;
  optional?: boolean;
}

function Select(options: SelectOptions): SelectElement;
Example:
Select({
  id: "priority",
  label: "Priority Level",
  placeholder: "Choose priority",
  options: [
    SelectOption({ label: "High", value: "high" }),
    SelectOption({ label: "Medium", value: "medium" }),
    SelectOption({ label: "Low", value: "low" })
  ]
})

RadioSelect

Radio button group.
id
string
required
Unique input ID
label
string
required
Radio group label
options
SelectOptionElement[]
required
Available options
initialOption
string
Pre-selected option value
optional
boolean
If true, field is not required for submission
interface RadioSelectOptions {
  id: string;
  label: string;
  options: SelectOptionElement[];
  initialOption?: string;
  optional?: boolean;
}

function RadioSelect(options: RadioSelectOptions): RadioSelectElement;
Example:
RadioSelect({
  id: "status",
  label: "Status",
  options: [
    SelectOption({ label: "Active", value: "active", description: "Currently running" }),
    SelectOption({ label: "Paused", value: "paused", description: "Temporarily stopped" }),
    SelectOption({ label: "Stopped", value: "stopped", description: "Permanently stopped" })
  ],
  initialOption: "active"
})

SelectOption

Option for Select or RadioSelect.
label
string
required
Display text
value
string
required
Value returned on submission
description
string
Optional description text
function SelectOption(options: {
  label: string;
  value: string;
  description?: string;
}): SelectOptionElement;

ModalSubmitEvent

Event fired when user submits the modal.
interface ModalSubmitEvent<TRawMessage = unknown> {
  /** The adapter that received this event */
  adapter: Adapter;
  /** The callback ID specified when creating the modal */
  callbackId: string;
  /** The user who submitted the modal */
  user: Author;
  /** Form field values keyed by input ID */
  values: Record<string, string>;
  /** Platform-specific view/dialog ID */
  viewId: string;
  /** Raw platform-specific payload */
  raw: unknown;
  /**
   * The private metadata string set when the modal was created.
   * Use this to pass arbitrary context (e.g., JSON) through the modal lifecycle.
   */
  privateMetadata?: string;
  /**
   * The thread where the modal was originally triggered from.
   * Available when the modal was opened via ActionEvent.openModal().
   */
  relatedThread?: Thread<Record<string, unknown>, TRawMessage>;
  /**
   * The message that contained the action which opened the modal.
   * Available when the modal was opened from a message action via ActionEvent.openModal().
   * This is a SentMessage with edit/delete capabilities.
   */
  relatedMessage?: SentMessage<TRawMessage>;
  /**
   * The channel where the modal was originally triggered from.
   * Available when the modal was opened via SlashCommandEvent.openModal().
   */
  relatedChannel?: Channel<Record<string, unknown>, TRawMessage>;
}
Handler:
chat.onModalSubmit("form_id", async (event) => {
  const name = event.values.name;
  const email = event.values.email;
  
  // Process the form
  await processForm({ name, email });
  
  // Optionally respond to update the modal
  return { action: "close" };
});

ModalCloseEvent

Event fired when user closes modal without submitting (requires notifyOnClose: true).
interface ModalCloseEvent<TRawMessage = unknown> {
  adapter: Adapter;
  callbackId: string;
  user: Author;
  viewId: string;
  raw: unknown;
  privateMetadata?: string;
  relatedThread?: Thread<Record<string, unknown>, TRawMessage>;
  relatedMessage?: SentMessage<TRawMessage>;
  relatedChannel?: Channel<Record<string, unknown>, TRawMessage>;
}
Handler:
chat.onModalClose("form_id", async (event) => {
  console.log(`${event.user.userName} closed the modal without submitting`);
});
Return these from onModalSubmit handlers to control modal behavior:

Close Modal

return { action: "close" };

Show Validation Errors

return {
  action: "errors",
  errors: {
    email: "Invalid email format",
    phone: "Phone number is required"
  }
};

Update Modal Contents

return {
  action: "update",
  modal: Modal({
    callbackId: "form_updated",
    title: "Updated Form",
    children: [/* new fields */]
  })
};

Push New Modal (Stack)

return {
  action: "push",
  modal: Modal({
    callbackId: "confirmation",
    title: "Confirm",
    children: [/* confirmation fields */]
  })
};

Complete Example

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

// Open modal from button click
chat.onAction("create_ticket", async (event) => {
  await event.openModal(
    Modal({
      callbackId: "ticket_form",
      title: "Create Support Ticket",
      submitLabel: "Create",
      notifyOnClose: true,
      children: [
        TextInput({
          id: "subject",
          label: "Subject",
          placeholder: "Brief description"
        }),
        TextInput({
          id: "description",
          label: "Description",
          multiline: true,
          maxLength: 1000
        }),
        Select({
          id: "priority",
          label: "Priority",
          options: [
            SelectOption({ label: "High", value: "high" }),
            SelectOption({ label: "Medium", value: "medium" }),
            SelectOption({ label: "Low", value: "low" })
          ],
          initialOption: "medium"
        }),
        RadioSelect({
          id: "category",
          label: "Category",
          options: [
            SelectOption({ label: "Bug", value: "bug" }),
            SelectOption({ label: "Feature", value: "feature" }),
            SelectOption({ label: "Question", value: "question" })
          ]
        })
      ]
    })
  );
});

// Handle submission
chat.onModalSubmit("ticket_form", async (event) => {
  const { subject, description, priority, category } = event.values;
  
  // Validate
  if (!subject || subject.length < 5) {
    return {
      action: "errors",
      errors: { subject: "Subject must be at least 5 characters" }
    };
  }
  
  // Create ticket
  const ticket = await createTicket({
    subject,
    description,
    priority,
    category,
    author: event.user
  });
  
  // Post confirmation to the thread
  if (event.relatedThread) {
    await event.relatedThread.post(
      `Ticket #${ticket.id} created: ${subject}`
    );
  }
  
  // Close the modal
  return { action: "close" };
});

// Handle close without submit
chat.onModalClose("ticket_form", async (event) => {
  console.log(`${event.user.userName} cancelled ticket creation`);
});

JSX Support

Modals also support JSX syntax:
/** @jsxImportSource chat */
import { Modal, TextInput, Select, SelectOption } from "chat";

await event.openModal(
  <Modal callbackId="feedback" title="Feedback" submitLabel="Send">
    <TextInput id="message" label="Your feedback" multiline />
    <Select id="rating" label="Rating">
      <SelectOption label="Excellent" value="5" />
      <SelectOption label="Good" value="4" />
      <SelectOption label="Fair" value="3" />
    </Select>
  </Modal>
);