A modal is the Slack app equivalent of an alert box, pop-up, or dialog box. Modals capture and maintain focus within Slack until the user submits or dismisses the modal. This makes them a powerful piece of app functionality for engaging with users. Modals are available for both Slack apps and workflow automations.
You can use modals with other app surfaces such as messages and Home tabs.
For example, you could have an app present a task dashboard that resides in the app's Home tab. A user clicks a button to add a task, and is presented with a modal to input some plain text and select from a list of categories. Upon submitting, a message is sent to a triage channel in the Slack workspace, where another user can click a button to claim the task.
Each modal consists of some standardized UI elements — a title, an x button to dismiss the modal, a cancel button — that wrap around a focused space, known as the modal's view.
To generate a modal, an app will compose an initial view. Apps can compose view layouts and add interactivity to views using Block Kit.
A modal can hold up to 3 views at a time in a view stack. There is only ever a single view visible at a given moment, but the view stack can retain previous views, returning to them with their prior state still intact. An app can push new views onto a modal's view stack, or update an existing view within that stack — including the currently visible view.
If you've used our outmoded dialogs in your apps, check out our guide to upgrading dialogs to modals.
In the beginning, there was a user interaction.
Interactions happen with one of an app's entry points. As a result, the app is sent an interaction payload containing a special trigger_id
. The app then composes an initial view (view A in the diagram below).
The user interacts with an interactive component in view A. This sends another interaction payload to the app. The app uses the context from this new payload to update the currently visible view A with additional content.
The user interacts with another interactive component in view A, and another interaction payload is sent to the app. The app uses the context from the new payload to push a new view (view B) on to the modal's view stack, causing it to appear to the user immediately. View A remains in the view stack, but is no longer visible or active. The user enters some values into input blocks in view B, and clicks the view's submit button. This sends a different type of interaction payload to the app.
The app handles the view submission and responds by clearing the view stack.
As described above, the view stack can be manipulated in a few ways over the course of a modal's lifetime:
In order to capture user input, a special type of Block Kit component is available called an input block. An input block can hold a plain-text input, a select menu, or a multi-select menu. Plain-text inputs can be set to accept single or multi-line text. See the full input block definition in the input block reference.
If you're using any input blocks, you must include the submit
field when defining your view.
Before we begin, if you don't already have a Slack app, click the following button to create one:
You'll also need an access token — read our guide to app distribution to see how you can generate one. And, you'll need to enable Interactive Components in your app's management dashboard. We explain all about how to enable these settings, what interaction payloads and request URLs are, and how to handle interactivity in our guide to handling user interaction.
Alternatively, you could create a workflow automation with the quickstart guide.
Before opening a modal, you'll need to define a view object to structure the layout of the initial view. The view object is a JSON object that defines the content populating this initial view and some version of metadata about the modal itself.
Modal view objects are used within the following Web API methods:
Field | Type | Description |
---|---|---|
type |
String | Required. The type of view. Set to modal for modals. |
title |
Object | Required. The title that appears in the top-left of the modal. Must be a plain_text text element with a max length of 24 characters. |
blocks |
Array | Required. An array of blocks that defines the content of the view. Max of 100 blocks. |
close |
Object | An optional plain_text element that defines the text displayed in the close button at the bottom-right of the view. Max length of 24 characters. |
submit |
Object | An optional plain_text element that defines the text displayed in the submit button at the bottom-right of the view. submit is required when an input block is within the blocks array. Max length of 24 characters. |
private_metadata |
String | An optional string that will be sent to your app in view_submission and block_actions events. Max length of 3000 characters. |
callback_id |
String | An identifier to recognize interactions and submissions of this particular view. Don't use this to store sensitive information (use private_metadata instead). Max length of 255 characters. |
clear_on_close |
Boolean | When set to true , clicking on the close button will clear all views in a modal and close it. Defaults to false . |
notify_on_close |
Boolean | Indicates whether Slack will send your request URL a view_closed event when a user clicks the close button. Defaults to false . |
external_id |
String | A custom identifier that must be unique for all views on a per-team basis. |
submit_disabled |
Boolean | When set to true , disables the submit button until the user has completed one or more inputs. This property is for configuration modals. |
If you use non-standard characters (including characters with diacritics), please be aware that these are converted and sent in unicode format when you receive the view callback payloads.
Preserving input
entry in views
Data entered or selected in input
blocks can be preserved while updating views. The new view
that you use with views.update
should contain the same input blocks and elements with identical block_id
and action_id
values.
Below is a full modal view example. We'll construct this piece by piece in the following sections.
{
"type": "modal",
"title": {
"type": "plain_text",
"text": "Modal title"
},
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "It's Block Kit...but _in a modal_"
},
"block_id": "section1",
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Click me"
},
"action_id": "button_abc",
"value": "Button value",
"style": "danger"
}
},
{
"type": "input",
"label": {
"type": "plain_text",
"text": "Input label"
},
"element": {
"type": "plain_text_input",
"action_id": "input1",
"placeholder": {
"type": "plain_text",
"text": "Type in here"
},
"multiline": false
},
"optional": false
}
],
"close": {
"type": "plain_text",
"text": "Cancel"
},
"submit": {
"type": "plain_text",
"text": "Save"
},
"private_metadata": "Shhhhhhhh",
"callback_id": "view_identifier_12"
}
The layout of a view is composed using Block Kit's visual and interactive components — including special input blocks to gather user input.
These visual components are all contained within the blocks
field in your view object. Read our comprehensive guide to composing layouts with Block Kit to see how the blocks
array should be formed.
When creating a view, set a unique block_id
for each block and a unique action_id
for each block element. This will make it much easier to track the possible values of those block elements when they are returned in view_submission
payloads.
If you're using any input blocks, you must include a submit
field in your view object.
Once you've created your blocks
layout, you need to add it to your view
object payload. Here's an example view
that we'll use:
{
"type": "modal",
"callback_id": "modal-identifier",
"title": {
"type": "plain_text",
"text": "Just a modal"
},
"blocks": [
{
"type": "section",
"block_id": "section-identifier",
"text": {
"type": "mrkdwn",
"text": "*Welcome* to ~my~ Block Kit _modal_!"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Just a button",
},
"action_id": "button-identifier",
}
}
],
}
Your modal's initial view is now ready for use.
To open a new modal, your app must possess a valid, unexpired trigger_id
, obtained from an interaction payload. Your app will receive one of these payloads, and therefore a trigger_id
, after a user invokes one of the app's entry points. If your app doesn't have one of these entry point features enabled, the app will not be able to open a modal. The trigger_id
requirement ensures that modals only appear when apps have the express permission of a user.
A trigger_id
will expire 3 seconds after it's sent to your app, so you’ll want to use it quickly.
Once in possession of a trigger_id
, your app can call views.open
with the view
payload you created above:
POST https://slack.com/api/views.open
Content-type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN_HERE
{
"trigger_id": "156772938.1827394",
"view": {
"type": "modal",
"callback_id": "modal-identifier",
"title": {
"type": "plain_text",
"text": "Just a modal"
},
"blocks": [
{
"type": "section",
"block_id": "section-identifier",
"text": {
"type": "mrkdwn",
"text": "*Welcome* to ~my~ Block Kit _modal_!"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Just a button"
},
"action_id": "button-identifier"
}
}
]
}
}
This will open a new modal, and display the view you composed within it. If the view was opened successfully, your app will receive a response containing an ok
value set to true
, along with the view object that was displayed to the user. There's an example response in the views.open
reference.
When you receive this success response, you'll want to store the view.id
from it for safekeeping. This will allow you to update the contents of that view later on.
Depending on how your modal's initial view was composed, there are a few different interactions that could happen:
block_actions
payloads. When someone uses an interactive component in your app's views, the app receives a block_actions
payload. This does not apply to components included in an input
block (see below for details about those). Once processed, the information in the block_actions
payload can be used to respond to the interaction.view_submission
payloads. When a view is submitted, you'll receive a view_submission
payload. This payload will contain a state
object with the values and contents of any stateful blocks that were in the submitted view. Refer to the info on view.state.values
of the view_submission
payload to understand the structure of this state
object. As with block_actions
payloads, the information in view_submission
payloads can be used to respond.view_closed
payloads. Your app can optionally receive view_closed
payloads whenever a user clicks on the Cancel or x buttons. These buttons are standard in all app modals. To receive the view_closed
payload when this happens, set notify_on_close
to true
when creating a view with views.open
, pushing a new view with views.push
, or in your response to the action.If a user closes a specific view in a modal using the Cancel button, you will receive a view_closed
event with the corresponding view's id
. However, if the user exits the modal with the x button in the top-right corner, you'll receive a view_closed
event with the initial modal view's id
and the is_cleared
flag set to true
.
Upon receiving either of the interaction payloads described above, your app can choose from a multitude of responses. In every case, apps must return a required acknowledgment response back to the HTTP request that sent the payload.
It's likely you'll also want your app to modify the modal itself in some way. If so, you have a few options depending on the type of interaction that occurred:
block_actions
interaction, your app must send the acknowledgment response. Then the app can use the view.*
API endpoints explained below to make desired modifications.view_submission
interaction, your app can include a valid response_action
with the acknowledgment response. We'll explain how to do that below.A view can be updated to change the layout or its underlying state. This update can occur whether or not the view is currently visible within the modal's view stack. There are two ways to update a view in a modal:
response_action
If your app just received a view_submission
payload, you have 3 seconds to respond and update the source view. Respond to the HTTP request app with a response_action
field of value update
, along with a newly composed view
as in the following example:
{
"response_action": "update",
"view": {
"type": "modal",
"title": {
"type": "plain_text",
"text": "Updated view"
},
"blocks": [
{
"type": "section",
"text": {
"type": "plain_text",
"text": "I've changed and I'll never be the same. You must believe me."
}
}
]
}
}
This method only works in response to a user clicking the submit
button in a view; therefore it can only be used to update the currently visible view.
Remember the view.id
included in the success response when you used views.open
earlier? Hopefully you have that id
handy, because you can now use it to update the view.
You may update a modal view by calling views.update
. Include a newly-composed view
and the id
of the view that should be updated. This new view will replace the contents of the existing view.
Preserving input
entry
Data entered or selected in input
blocks can be preserved while updating views. The new view
object that you use with views.update
should contain the same input blocks and elements with identical block_id
and action_id
values.
Here's an example of a views.update
call:
POST https://slack.com/api/views.update
Content-type: application/json
Authorization: Bearer YOUR_TOKEN_HERE
{
"view_id": "VIEW ID FROM VIEWS.OPEN RESPONSE",
"hash": "156772938.1827394",
"view": {
"type": "modal",
"callback_id": "view-helpdesk",
"title": {
"type": "plain_text",
"text": "Submit an issue"
},
"submit": {
"type": "plain_text",
"text": "Submit"
},
"blocks": [
{
"type": "input",
"block_id": "ticket-title",
"label": {
"type": "plain_text",
"text": "Ticket title"
},
"element": {
"type": "plain_text_input",
"action_id": "ticket-title-value"
}
},
{
"type": "input",
"block_id": "ticket-desc",
"label": {
"type": "plain_text",
"text": "Ticket description"
},
"element": {
"type": "plain_text_input",
"multiline": true,
"action_id": "ticket-desc-value"
}
}
]
}
}
views.update
Race conditions can potentially occur when updating views using views.update
, but luckily there's a solution built-in.
Let's digress for an example:
views.update
with a new view
. In this case, the modal will correctly display task A as complete, and task B as incomplete.views.update
call from completing task A could take longer to complete than the same API call from completing task B. Perhaps it took longer to query for the list of tasks after marking task A as complete, or perhaps temporary network conditions slowed down the API call to views.update
for task A.views.update
call from task B.views.update
call from task A, and the user would see task A as complete, but task B as incomplete.views.update
call from after completing task A has overwritten the up-to-date views.update
call from after completing task B.Okay, digression over. So, how can your app solve this problem?
To prevent these kinds of conditions, there is a hash
value included in all block_actions
payloads. You can pass hash
when calling views.update
. If the hash
is outdated, the API call will be rejected. This provides an automated assurance that you will never accidentally update a view with outdated data. We highly recommend your apps take advantage of this hash
value.
Within a modal's view stack, 3 views can exist at any one time. If there is still space remaining, you can push a new view onto the view stack. The newly-pushed view will immediately become visible to the user. When the user closes or submits this new view, they'll return to the next one down in the stack.
There are two ways to add a view to a modal's view stack:
response_action
If your app has received a view_submission
payload, you have 3 seconds to respond and push a new view. Respond to the HTTP request app with a response_action
field of value push
, along with a newly composed view
as in the following example:
{
"response_action": "push",
"view": {
"type": "modal",
"title": {
"type": "plain_text",
"text": "Updated view"
},
"blocks": [
{
"type": "image",
"image_url": "https://api.slack.com/img/blocks/bkb_template_images/plants.png",
"alt_text": "Plants"
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "_Two of the author's cats sit aloof from the austere challenges of modern society_"
}
]
}
]
}
}
The view immediately becomes visible on top of the submitted view, adding it to the top of the modal's view stack. When a user submits or cancels the current view, they’ll return to the previous view on the stack.
If you need to get the id
of the newly-pushed view (rather than the id
of the submitted view, which is what view.id
will return) in response to a view_submission
payload, you can pass an external_id
to update the modal after the new view is pushed. You can then use the external_id
to track your new view and update it using views.update
.
The views.push
method will add a new view to the top of the current stack of views in a modal, requires a trigger_id
(similar to views.open
), and can only be called when a modal is already open. Therefore, the only possible way to acquire a trigger_id
to use here is from the use of an interactive component in the modal.
A trigger_id
will expire 3 seconds after you receive it, so you need to make your call to views.push
in that 3 second window.
POST https://slack.com/api/views.push
Content-type: application/json
Authorization: Bearer YOUR_TOKEN_HERE
{
"trigger_id": "YOUR TRIGGER ID",
"view": {
"type": "modal",
"callback_id": "edit-task",
"title": {
"type": "plain_text",
"text": "Edit task details"
},
"submit": {
"type": "plain_text",
"text": "Create"
},
"blocks": [
{
"type": "input",
"block_id": "edit-task-title",
"label": {
"type": "plain_text",
"text": "Task title"
},
"element": {
"type": "plain_text_input",
"action_id": "task-title-value",
"initial_value": "Block Kit documentation"
},
},
{
"type": "input",
"block_id": "edit-ticket-desc",
"label": {
"type": "plain_text",
"text": "Ticket description"
},
"element": {
"type": "plain_text_input",
"multiline": true,
"action_id": "ticket-desc-value",
"initial_value": "Update Block Kit documentation to include Block Kit in new surface areas (like modals)."
}
}
]
}
}
The view immediately becomes visible on top of the submitted view, adding it to the top of the modal's view stack. When a user submits or cancels the current view, they’ll return to the previous view on the stack.
A successful response from views.push
will include an id
for the newly pushed view. This id
is useful if you need to update the view using views.update
.
Apps have the ability to close views within a modal. This can happen only in response to the user clicking a submit button in the modal, sending the view_submission
payload. After receiving this payload, your app has 3 seconds to respond and close the submitted view, or close all views.
Your app cannot use any other method to close views. A user may choose to cancel a view, or close the entire modal by clicking on the cancel or x buttons, and your app can optionally receive a notification if that happens.
If your app responds to a view_submission
event with a basic acknowledgment response — an HTTP 200 response — this will immediately close the submitted view and remove it from the view stack. Your HTTP 200 response must be empty for this step to complete successfully.
If there are no more views left in the stack, the modal will close. Otherwise, the modal will display the next view down in the stack.
To close all views, set the response_action
to clear
. Regardless of the number of views in the stack, it will be emptied, and the modal will close:
{
"response_action": "clear"
}
Upon receiving a view_submission
event, your app may want to validate any inputs from the submitted view.
If your app detects any validation errors, say an invalid email or an empty required field, the app can respond to the payload with a response_action
of errors
and an errors
object providing error messages:
{
"response_action": "errors",
"errors": {
"ticket-due-date": "You may not select a due date in the past"
}
}
Within the errors
object, you supply a key that is the block_id
of the erroneous input block, and a value - the plain text error message to be displayed to the user.
The above JSON object would highlight the error within the modal around the ticket-due-date
block, displaying the chosen error message. The user can then edit their input and resubmit the view.
Your app is responsible for setting and tracking block_id
s when composing views.
Because views within a modal are usually connected in purpose, your app may want a way to send data from one view into the other, and then back again once a view is submitted.
To do this, we provide an optional private_metadata
parameter that can be supplied in a view
payload when your app opens a modal with an initial view, or updates an existing view.
This private_metadata
string is not shown to users, but is returned to your app in view_submission
and block_actions
events. Refer to private_metadata
in view
payloads for more detail.
You may want to publish a message to a Slack channel after your modal is submitted by doing one of the following:
Unless your app is already configured to do one of the first two things, you'll want to include specific blocks and a special parameter in your modal.This method provides a straightforward route to message publishing for certain apps that use modals, such as those also using shortcuts. Here's how:
blocks
array that contains either a conversations_select
or channels_select
element.response_url_enabled
parameter in the select menu to true
. This field only works with menus in input blocks in modals.Here's an example view object containing this special configuration:
{
"type": "modal",
...
"blocks": [
...
{
"block_id": "my_block_id",
"type": "input",
"optional": true,
"label": {
"type": "plain_text",
"text": "Select a channel to post the result on",
},
"element": {
"action_id": "my_action_id",
"type": "conversations_select",
"response_url_enabled": true,
},
},
],
};
}
When a user opens the modal, they can choose a conversation to which they'd like a message posted. If you supply the default_to_current_conversation
parameter for the conversation_select
element, you can even pre-populate the conversation they're currently viewing.
Once they submit the view, you'll receive a view_submission
payload that will now include response_urls
. You can use the values in response_urls
to publish message responses. Refer to our guide to handling interactions.
We recommend keeping these best practices in mind while using this technique:
optional
to true
to allow the user to decline selecting a conversation.Modals are intended for short-term interaction. Their popup, attention-grabbing nature makes them a mighty weapon that should be wielded only at truly appropriate moments.
Your app can't invoke a modal without a trigger_id
from a user interaction, which creates a certain amount of intentional usage. That being said, make sure to not surprise your users - they should understand that a modal will open based on their action.
Once invoked, a user can cancel a modal at any time. Handle these cancellations with grace. Don't try to force the user to proceed through the same process again.
Whatever the modal's content, the end-user shouldn’t need to spend excessive time on a single view within a modal. Rather than overloading a view, your app should use inputs sparingly, and implement pagination as necessary — generally when there are upwards of six inputs or blocks of information.
Your app should indicate what happens upon a modal submission. For example, if a message will be posted into a channel on the users behalf, it should be evident.
If your app needs to perform resource-intensive data fetching, you should implement a temporary loading screen so the user better understands what's happening. This is especially important to consider as your app is installed on workspaces for larger teams with even larger collections of data.
Users should not be prompted for confidential information like passwords within modals (or any app surface area, for that matter). When your app needs access to a user’s credentials, you should direct them to your login and store any necessary information on your app’s backend.