Modal Forms and Dialogs
Modals allow you to collect structured input from users through form dialogs. They can be opened from button clicks, slash commands, or other interactions.Modal Structure
interface ModalElement {
type: "modal";
callbackId: string; // Unique ID for routing submissions
title: string; // Modal title
submitLabel?: string; // Submit button text (default: "Submit")
closeLabel?: string; // Close button text (default: "Cancel")
notifyOnClose?: boolean; // Fire event when closed without submitting
privateMetadata?: string; // Pass context through the modal lifecycle
children: ModalChild[]; // Form inputs
}
Opening Modals
From Button Actions
chat.onAction("open_form", async (event) => {
await event.openModal(
<Modal callbackId="feedback_form" title="Submit Feedback">
<TextInput id="feedback" label="Your feedback" multiline />
<Select
id="category"
label="Category"
options={[
{ label: "Bug Report", value: "bug" },
{ label: "Feature Request", value: "feature" },
{ label: "Question", value: "question" },
]}
/>
</Modal>
);
});
From Slash Commands
chat.onSlashCommand("/feedback", async (event) => {
await event.openModal(
<Modal
callbackId="feedback_form"
title="Submit Feedback"
submitLabel="Send"
closeLabel="Dismiss"
>
<TextInput
id="message"
label="Message"
placeholder="Tell us what you think..."
multiline
/>
</Modal>
);
});
Form Input Elements
TextInput
Single or multi-line text input.interface TextInputElement {
type: "text_input";
id: string;
label: string;
placeholder?: string;
initialValue?: string;
multiline?: boolean;
optional?: boolean;
maxLength?: number;
}
<TextInput
id="name"
label="Full Name"
placeholder="John Doe"
maxLength={100}
/>
<TextInput
id="description"
label="Description"
multiline
optional
placeholder="Provide additional details..."
/>
Select
Dropdown selection (single choice).interface SelectElement {
type: "select";
id: string;
label: string;
options: SelectOptionElement[];
placeholder?: string;
initialOption?: string; // Value of pre-selected option
optional?: boolean;
}
interface SelectOptionElement {
label: string;
value: string;
description?: string;
}
<Select
id="priority"
label="Priority Level"
placeholder="Choose priority..."
initialOption="medium"
options={[
{ label: "Low", value: "low", description: "Non-urgent" },
{ label: "Medium", value: "medium", description: "Normal priority" },
{ label: "High", value: "high", description: "Urgent" },
]}
/>
Select({
id: "status",
label: "Status",
options: [
SelectOption({ label: "Active", value: "active" }),
SelectOption({ label: "Inactive", value: "inactive" }),
],
})
RadioSelect
Radio button group (single choice).interface RadioSelectElement {
type: "radio_select";
id: string;
label: string;
options: SelectOptionElement[];
initialOption?: string;
optional?: boolean;
}
<RadioSelect
id="notification"
label="Notification Preference"
initialOption="email"
options={[
{ label: "Email", value: "email" },
{ label: "Slack", value: "slack" },
{ label: "None", value: "none" },
]}
/>
Handling Modal Submissions
Register a handler for the modal’scallbackId:
chat.onModalSubmit("feedback_form", async (event) => {
// Access form values
const feedback = event.values.feedback;
const category = event.values.category;
// Process the submission
await saveFeedback({
user: event.user.userId,
feedback,
category,
});
// Post confirmation to the related thread (if opened from action)
if (event.relatedThread) {
await event.relatedThread.post(
`Thank you for your ${category} feedback!`
);
}
// Close the modal (default behavior if no response returned)
});
Modal Submit Event
interface ModalSubmitEvent {
callbackId: string;
values: Record<string, string>; // Input values by ID
user: Author; // Who submitted
viewId: string; // Platform-specific view ID
privateMetadata?: string; // Context you passed in
// Context from where modal was opened
relatedThread?: Thread; // From ActionEvent.openModal()
relatedMessage?: SentMessage; // The message with the button
relatedChannel?: Channel; // From SlashCommandEvent.openModal()
adapter: Adapter;
raw: unknown;
}
Modal Responses
Validation Errors
Show field-specific errors without closing the modal:chat.onModalSubmit("user_form", async (event) => {
const email = event.values.email;
if (!email.includes("@")) {
return {
action: "errors",
errors: {
email: "Please enter a valid email address",
},
};
}
// Process valid submission...
});
Update Modal
Update the modal content (for multi-step flows):chat.onModalSubmit("step1", async (event) => {
return {
action: "update",
modal: Modal({
callbackId: "step2",
title: "Step 2 of 2",
children: [
TextInput({ id: "details", label: "Additional Details" }),
],
}),
};
});
Push New Modal
Stack a new modal on top (Slack only):chat.onModalSubmit("create_item", async (event) => {
return {
action: "push",
modal: Modal({
callbackId: "confirm_create",
title: "Confirm Creation",
children: [
Text("Are you sure you want to create this item?"),
],
}),
};
});
Close Modal
Explicitly close the modal:chat.onModalSubmit("cancel_form", async (event) => {
return { action: "close" };
});
Private Metadata
Pass context through the modal lifecycle:// Open modal with context
chat.onAction("edit_order", async (event) => {
const orderId = event.value;
await event.openModal(
<Modal
callbackId="edit_order_form"
title="Edit Order"
privateMetadata={JSON.stringify({ orderId, threadId: event.threadId })}
>
<TextInput id="quantity" label="Quantity" />
</Modal>
);
});
// Retrieve context in handler
chat.onModalSubmit("edit_order_form", async (event) => {
const { orderId, threadId } = JSON.parse(event.privateMetadata || "{}");
const quantity = event.values.quantity;
await updateOrder(orderId, { quantity });
});
Handling Modal Close
Listen for modal dismissal (requiresnotifyOnClose: true):
chat.onModalClose("feedback_form", async (event) => {
console.log(`User ${event.user.userName} closed the feedback form`);
});
interface ModalCloseEvent {
callbackId: string;
user: Author;
viewId: string;
privateMetadata?: string;
relatedThread?: Thread;
relatedMessage?: SentMessage;
relatedChannel?: Channel;
adapter: Adapter;
raw: unknown;
}
Complete Example
Full workflow with validation and confirmation:import { Chat, Modal, TextInput, Select, SelectOption, Card, Text, Actions, Button } from "chat";
const chat = new Chat({ /* ... */ });
// Trigger modal from command
chat.onSlashCommand("/create-ticket", async (event) => {
await event.openModal(
<Modal
callbackId="create_ticket"
title="Create Support Ticket"
submitLabel="Create"
notifyOnClose
>
<TextInput
id="subject"
label="Subject"
placeholder="Brief description..."
maxLength={100}
/>
<TextInput
id="description"
label="Description"
multiline
placeholder="Detailed explanation..."
/>
<Select
id="priority"
label="Priority"
options={[
{ label: "Low", value: "low" },
{ label: "Medium", value: "medium" },
{ label: "High", value: "high" },
]}
/>
</Modal>
);
});
// Handle submission with validation
chat.onModalSubmit("create_ticket", async (event) => {
const { subject, description, priority } = event.values;
// Validate
if (subject.length < 5) {
return {
action: "errors",
errors: {
subject: "Subject must be at least 5 characters",
},
};
}
// Create ticket
const ticket = await createTicket({
subject,
description,
priority,
userId: event.user.userId,
});
// Post confirmation to channel
if (event.relatedChannel) {
await event.relatedChannel.post(
<Card title="Ticket Created">
<Text style="bold">Ticket #{ticket.id}</Text>
<Text>{subject}</Text>
<Text style="muted">Priority: {priority}</Text>
</Card>
);
}
// Modal closes automatically
});
// Track cancellations
chat.onModalClose("create_ticket", async (event) => {
console.log(`Ticket creation cancelled by ${event.user.userName}`);
});
Platform Support
- Slack: Full support (update, push, errors)
- Microsoft Teams: Basic support (submit/close only)
- Google Chat: Card-based dialogs
Next Steps
Actions
Learn how to trigger modals from buttons
Slash Commands
Open modals from slash commands