Modals

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.


Understanding the lifecycle of a modal

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

A diagram explaining the view stack through the lifecycle of a modal

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:

  • Updating a view. This can happen at any time while the modal is open. Updates can change the contents and layout of the view. A view update should normally only happen in response to the use of interactive components or inputs gathered in the view.
  • Adding a new view. Apps can push a new view onto the modal's view stack. This causes the new view to immediately become visible. Three views can exist in the view stack at any one time. Again, pushing a new view should normally only happen in response to the use of interactive components or inputs gathered in one of the previous views.
  • Closing a view. When a view contains inputs, users can submit the modal when that view is visible. The app can then either close that specific view or all views in the view stack. Closing a single view removes it from the view stack and causes the next view in the stack to appear again. Closing all views will close the modal entirely.

Gathering user input

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.

Preparing your app

Before we begin, if you don't already have a Slack app, click the following button to create one:

Create your Slack app

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.

Composing modal views

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.

Defining modal view objects

Modal view objects are used within the following Web API methods:

Modal view object fields

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.

Example

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.

An example of a modal across iOS, Web, and Android

View an example

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.

Opening modals

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.


Handling and responding to interactions

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:

  • If you want to modify a modal in response to a 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.
  • If you want to modify a modal in response to a view_submission interaction, your app can include a valid response_action with the acknowledgment response. We'll explain how to do that below.

Updating modal views

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:

Update a view via 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.

Update a view via API

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"
        }
      }
    ]
  }
}

Avoiding race conditions when using 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:

  • Suppose there is a view with a list of tasks that can be marked as complete using a button. When the task is completed, the UI shows the timestamp when the task was completed.
  • If the user clicks the complete button for task A, the app will mark the task as completed in the app's database, query the same database for an up-to-date list of tasks, then make a call to views.update with a new view. In this case, the modal will correctly display task A as complete, and task B as incomplete.
  • If, while the above processing is happening, the user clicks the complete button for task B, the app will go through the same process as above. All being well, the view will correctly display both task A and task B as complete.
  • However, it's possible that the 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.
  • In this case, the user would initially see the correct task list display after the views.update call from task B.
  • The modal would then update again after the views.update call from task A, and the user would see task A as complete, but task B as incomplete.
  • The outdated 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.

Adding a new view

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:

Add a new view via 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.

Add a new view via API

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)."
        }
      }
    ]
  }
}

View this example

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.

Closing views

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.

Close the current view

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.

Close all views

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

Display errors in views

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_ids when composing views.

A modal that is rendering errors supplied by the developer

Carry data between 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:

*. Request necessary permissions and use the Web API *. Generate and use a webhook *. Include specific blocks and a special parameter in your modal

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:

  1. Compose a modal view with a blocks array that contains either a conversations_select or channels_select element.
  2. Set the response_url_enabled parameter in the select menu to true. This field only works with menus in input blocks in modals.
  3. Open and handle your modal as described in the guide above.

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:

  • Ensure you provide clear instruction to the user that a message will be posted to the conversation they choose.
  • Consider setting optional to true to allow the user to decline selecting a conversation.
  • If the user declines to select a conversation, or if they cancel the modal, handle this as appropriate.

Designing modals

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.

An example of modals in desktop and mobile clients

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.

Keep modals glanceable

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.

Indicate outcome

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.

Show progress

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.

Don’t prompt for login information

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.

Use cases

  • Collecting input from users

  • Displaying lists or results

  • Confirming a user’s action

  • Onboarding a user to your app

Recommended reading
Connect with us
Our developer relations team invites you to our home on LinkedIn.