Modals
Modals open form dialogs in response to button clicks or slash commands. They support text inputs, dropdowns, radio buttons, and server-side validation. Modals are currently supported on Slack and Teams.
Open a modal
Modals are opened from action handlers or slash command handlers using event.openModal():
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 |
callbackUrl | string (optional) | URL to POST form values to on submit |
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 |
DateInput
A date picker. Renders a Slack datepicker and an Adaptive Card Input.Date.
| 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 date as YYYY-MM-DD |
optional | boolean (optional) | Allow empty submission |
The submitted value is an ISO YYYY-MM-DD string. An initialValue that is not a valid YYYY-MM-DD date is ignored with a warning, because Slack rejects a malformed initial_date by refusing to open the modal at all.
NumberInput
A numeric field. Renders a Slack number_input and an Adaptive Card Input.Number.
| Prop | Type | Description |
|---|---|---|
id | string | Field identifier (key in event.values) |
label | string | Field label |
placeholder | string (optional) | Placeholder text |
initialValue | number (optional) | Pre-filled value |
min | number (optional) | Minimum accepted value |
max | number (optional) | Maximum accepted value |
decimal | boolean (optional) | Allow decimals (defaults to integers only) |
optional | boolean (optional) | Allow empty submission |
Like every other field, the submitted value arrives in event.values as a string. Parse it with Number(...) if you need a number.
decimal is enforced by Slack. Adaptive Cards has no decimal switch, so Input.Number accepts decimals on Teams regardless. Validate server-side if the distinction matters.
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 |
dispatchAction | boolean (optional) | Send selection changes to onAction in Slack modals (default: false) |
ExternalSelect
A dropdown that loads its options dynamically from a handler as the user types. Use it for large or remotely stored option sets, such as people, tickets, or records, where a static <Select> would be impractical. Slack-only.
| Prop | Type | Description |
|---|---|---|
id | string | Field identifier (key in event.values) |
label | string | Field label |
placeholder | string (optional) | Placeholder text |
minQueryLength | number (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 the value string. For <ExternalSelect> it's the full { label, value } object because the loader hasn't run yet. |
optional | boolean (optional) | Allow empty submission |
Register the loader with onOptionsLoad:
The selected value arrives in event.values on submit, the same as for 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"):
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, so keep your loader fast by caching, prefetching, or narrowing the query server-side.
On Slack, ExternalSelect uses Slack's block_suggestion payload, which is dispatched to the Options Load URL. In your Slack app settings go to Interactivity & Shortcuts → Select 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 setting, typing into an external select returns no results and no error.
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 |
dispatchAction | boolean (optional) | Send selection changes to onAction in Slack modals (default: false) |
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 selection changes
In Slack modals, set dispatchAction on Select or RadioSelect to receive changes before submission:
Register bot.onAction("scope", handler) to handle the selection. event.actionId is the input ID and event.value is the selected option's value. Clearing an optional select produces an undefined event.value. Modal actions have no event.thread; the Slack view ID and form state are available in event.raw.
This maps to Slack's input block dispatch_action. It defaults to false, is ignored by other adapters, and does not change the values received by onModalSubmit. Text input change events are not supported by this option.
To change dependent fields, update the view using Slack's views.update API. Preserve the view's private_metadata and use its hash to protect against out-of-order updates. Keep the same input IDs for fields whose existing values should be preserved.
Handle submissions
Register a handler with onModalSubmit using the same callbackId:
Response types
| Response | Description |
|---|---|
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
| 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
To run code when a user cancels a modal, register onModalClose. The Modal component must set notifyOnClose:
Callback URLs
Like buttons, modals accept a callbackUrl. When the modal is submitted, the form values are POSTed to the URL:
The POST body for modal submissions:
Pass context with privateMetadata
Use privateMetadata to carry context from the button click through to the submit handler: