Go to Slack

Using modals in Slack apps

The rundown
Read first:An overview of modals
Read next:The message is the medium

Modals are a focused surface to collect data from users or display dynamic and interactive information. To users, modals appear as focused surfaces inside of Slack enabling brief, yet deep interactions with apps.

Modals can be assembled using the visual and interactive components found in Block Kit. Read this guide to learn how modals are invoked, how to compose their contents, and how to enable and handle complex interactivity.

Block Kit in modals

In our overview of modals, we introduced and explained the concept and flow of the modal view stack.

Here is that flow again, this time in a more technical form:

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

  1. A user interacts with an app entry point. This sends an interaction payload to the app.

  2. With the trigger_id from the payload, and a newly composed initial view (view A), the app uses views.open to initiate a modal.

  3. The user interacts with an interactive component in view A. This sends another interaction payload to the app.

  4. The app uses the context from this new payload to update the currently visible view A with some additional content.

  5. The user interacts with another interactive component in view A. Another interaction payload is sent to the app.

  6. This time the app uses the context from the new payload to push a new view (view B) onto 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.

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

  8. The app handles the view submission and responds by clearing the view stack.

In the following guide we'll break down all the steps within this flow, including alternative choices the app could have made in various steps.


Getting started with modals

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.


1. Preparing your app for modals

Upon submission of a modal view, an interaction payload will be sent to your app's request URL. In addition, if your views include any of Block Kit's interactive components, you'll also receive interaction payloads upon a component's use.

Therefore, to prepare your app for using modals, 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.


2. Composing views for modals

Before opening a modal, you'll need to compose a view payload to define the layout of the initial view.

The view payload is a JSON object that defines the content populating this initial view and some various of metadata about the modal itself. Consult our reference guide to view payloads to see the full list of fields to include.

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 payload. Read our comprehensive guide to composing layouts with Block Kit to see how the blocks array should be formed.

If you're using any input blocks, you must include a submit field in your view payload.

When creating a view, set unique block_ids for all blocks and unique action_ids 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.

Once you've created your blocks layout, you need to add it to your view object payload`.

Here's a simple 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.


3. Opening a modal

To open a new modal, your app must possess a valid 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.

View our interaction payloads reference guide to understand the full structure of the various payloads your app can receive. Complete our guide to user interaction to understand when and how your app will receive these payloads.

A trigger_id will expire 3 seconds after it's sent to your app, so you’ll want to use it quickly. trigger_ids are explained in greater depth in our guide to responding to user interactions.

Once in possession of a valid, unexpired 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 pop open a new modal, and display the view you composed within it.

If the view was opened without a hitch, your app will receive a response containing an ok value set to true along with the view payload that was displayed to the user. There's an example response in the views.open reference guide.

When you receive this success response, you'll want to store the view.id from it for safekeeping. This will let you update the contents of that view later on.


Handling interactions in modals

Depending on how your modal's initial view was composed, there are a few different interactions that could happen.

Handling block_actions payloads

When someone uses an interactive component in your app's modal views, the app will receive 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, which we'll explain later.

Handling view_submission payloads

When a modal view is submitted, you'll receive a view_submission payload.

This payload will contain a state object with the values and contents of any user-modified input blocks that were in the submitted view. Consult the view.state.values in the view_submission reference guide to understand the structure of this state object.

As with block_actions payloads, the information in view_submission payloads can be used to respond, as shown below.

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 response_actions, as shown below.

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


Responding to modal interactions

After receiving either of the interaction payloads described above from a modal source, your app can choose from an endless 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 couple of 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.


Modifying modals

There are a number of different modifications that an app can make to an open modal:

Updating a view

A view can be updated to change the layout or the underlying state. This update can happen 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:

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

{
  "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 hitting the submit button in a view, therefore it can only be used to update the currently visible view.

Updating views via API

Remember the view.id that was included in the success response when you used views.open earlier? We hope you kept it, because you can now use it to update that view.

To update, call views.update with a newly composed view that should replace the contents of the existing view:

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 an easy solution built-in.

Let's digress for a simple example (you can skip to the solution if you understand this problem already):

Imagine 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 hits 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, and 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 hits 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 over-written the up-to-date views.update call from after completing task B.

Okay, digression over. How can your app solve this problem?

To prevent these kinds of race 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 then the API call will be rejected.

This provides an automated assurance that you will never accidentally update a view with something outdated. We highly recommend your app takes advantage of this hash value.


Pushing a new view

Within a modal's view stack, multiple views can exist at any one time. If there is still space remaining, you can push a new view onto the modal's 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.

Your app may only stack 3 views (including the initial view used with views.open) in a modal at any one time.

There are two ways to push a view to a modal's view stack:

Pushing a new view via response_action

If your app just 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.

{
  "response_action": "push",
  "view": {
    "type": "modal",
    "title": {
      "type": "plain_text",
      "text": "Updated view"
    },
    "blocks": [
      {
        "type": "image",
        "image_url": "https://api.slack.com/img/a_very_cute_image_of_two_very_cute_cats.png",
        "alt_text": "Two cats being too cute"
      },
      {
        "type": "context",
        "elements": [
          {
            "type": "mrkdwn",
            "text": "_Two of the author's cats sit aloof from the austere challenges of modern society_"
          }
        ]
      }
    ]
  }
}

The pushed view will immediately become 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 go back to the previous view on the stack.

Pushing a new view via API

views.push will add a new view to the top of the current stack of views in a modal.

views.push 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.

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 pushed view will immediately become 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 go back to the previous view on the stack.


Closing views

An app has 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, 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.

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

If all views in the modal should be closed, set the response_action to clear:

{
  "response_action": "clear"
}

Regardless of the amount of views that were in the stack, it will be emptied, and the modal will close.


Displaying 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 and 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

Carrying 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 when 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.

Check out private_metadata in our reference guide to view payloads for more detail.


The continuum of app surfaces

A modal is a focused and isolated space for apps, but apps shouldn't use them in isolation. Use them with other app surfaces like messages and Home tabs to create a continuum of functionality that rivals any standalone app.

Imagine a task app that presents 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. After submitting, a message is sent to a triage channel elsewhere in the Slack workspace, where another user can click a button to claim the task.

Work through all the possibilities, and get some tips and inspiration, by reading our guides to planning Slack apps.