Go to Slack

Using Block Kit in modals

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.

After a user invokes an app, a modal 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

A modal is a UI container that holds one to many views. When a modal is created, it's instantiated with a single view. An app may push new views onto the modal, or update an existing view within the modal, but ultimately, the modal UI is persistent for the user until the developer closes all views or the user manually exits the modal.

Modals tend to conform to a similar flow. Here is that flow in diagram form, with the steps elucidated below it:

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

  1. An app implements a feature that allows users to invoke it — an interactive component, a slash command, or a message action.
  2. After the user invokes the app, the app will receive a request payload containing a trigger_id at its interactive components Request URL.
  3. Using the trigger_id, the app calls views.open with the type set to modal, in addition to a valid view payload.
    • 3b. The app may store the view_id from the views.open response payload to dynamically update the modal using views.update.
  4. If the view payload contains input blocks and a submit parameter, a user submits the modal and the input values are sent to the app’s Request URL.
    • 4b. After the user submits the modal, the app responds with a response_action which can populate any input errors or close the modal, among other options.

Preparing your app for modals

Before you start using modals, you'll need to add a request URL in your app's dashboard. This URL should be able to receive and handle payloads for several different interactive component events explained below.

When an app launches a modal that contains user inputs into the ether of Slack, it's required to tether a submit button to it. This indicates that Slack will send a view_submission event with the values of the inputs to your request URL after a user clicks the green submit button in the bottom-right of the modal.

Modals are a member of the same interactive component family that houses buttons, select menus, and datepickers. If your app uses any of these features, the request URL is likely already specified and Slack will send view_submission events to the same destination.

If you don’t have interactive components configured, navigate to your Apps page. On the sidebar, click the Interactive Components tab. Flip the toggle that enables interactivity, fill out the Request URL at the top of the page, then click the green Save Changes button in the bottom-right corner. There is more information contained in our guide to interactivity.


Creating modals

To open a new modal, your app must possess a valid trigger_id. These are contained in payloads that your app receives after a user invokes a slash command, clicks a button, interacts with a select menu, or fires a message action.

Here’s an example of a response payload from a button action that contains a trigger_id:

{
    "type": "block_actions",
    "team": { … },
    "user": { … },
    "trigger_id": "xxxxxxxxxxx.XXXXXXXXXX",
    "actions": [{
        "type": "button",
        "block_id": "help_actions",
        "action_id": "help_button",
        "text": { … },
        "action_ts": "1568628000.585299"
    }]
}

Composing a modal

An app can use a trigger_id to construct a JSON object called the view payload. The view payload defines the content that will populate the primary view of the modal and any additional information about the modal itself. This information includes the title, whether the submit button should appear, and any private metadata associated with the view.

The content of a view is composed using Block Kit's visual and interactive components. Views contain a special input block that allows your app to fetch freeform input from users inside of your app’s modals.

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

View this example

You can read our reference guide to view payloads to see detailed information about the JSON structure. For now, here's a simple example:

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

Gathering user input

Input blocks can be used within the blocks array when composing a view. 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.

Modal with a single-line and multi-line input

View this example

See the full input block definition in the input block reference.

Opening a modal

Once the view payload is composed, your app can call views.open with the payload and a valid trigger_id. This will 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 payload containing an ok value set to true along with the view payload that was displayed to the user:

{ 
  "ok": true,
  "view": { 
    "id": "VN4EY482G",
    "team_id": "T9M5SK1JMA",
    "type": "modal",
    "title": { 
      "type": "plain_text",
      "text": "Just a modal"
    },
    "close": {
      "type": "plain_text",
      "text": "Cancel"
    },
    "submit": null,
    "blocks": [ ... ],
    "private_metadata": "",
    "callback_id": "modal-identifier",
    "state": { "values": {} },
    "hash": "1568843014.01d284ba",
    "clear_on_close": false,
    "notify_on_close": false,
    "root_view_id": "VN4EY482G",
    "previous_view_id": null,
    "app_id": "AXX3321AQ",
    "bot_id": "BXXP7AM4A" 
  }
}

Triggers expire 3 seconds after they’re sent to your app, so you’ll want to call views.open quickly. trigger_ids are covered in greater depth in our guide to trigger_ids.

When you receive the view payload, you’ll probably want to store the view ID (view["id"] in the JSON object) for safekeeping. This will let you call views.update to update the contents of that view.


Handling interactions in modals

Any interactive components within your view payload will be handled in the same way as all Block Kit interactive components. In the context of a modal, here’s what the interactive flow might look like:

  1. A user interacts with an interactive component (a button, select menu, datepicker, or overflow menu) inside of your app’s modal
  2. Your app receives a request payload that contains information about the user that invoked the action and the view it’s contained within. The request payload will also contain a trigger_id that your app can use to push a new view onto the existing modal.
  3. Your app must acknowledge the interaction within 3 seconds to indicate you’ve received the event by responding with 200 OK.
  4. In response to this interaction, your app may want to update the existing view or push a new view to the modal.

input blocks do not trigger a request payload like interactive components do. Instead, you’ll handle validation and response for input blocks when the user submits the view that contains those inputs.

To read more about handling interactive events, read our guide to receiving interaction requests.


Updating views in a modal

A view can be updated at any time using the view_id that was included in the response payload to views.open. When calling views.update, you should pass a new view payload that will 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"
        }
      }
    ]
  }
}

View this example

Avoiding race conditions with hash

By default, views inside of a modal can occassionally lead to race conditions.

For example, pretend there is a view with a list of tasks that can be marked as complete using a button. When the user clicks the button associated with task A, your app updates task A and queries the database for an up-to-date list of tasks before calling views.update. If, while you're updating task A, the user also marks task B as complete, your app will perform the same steps — update task B, query the database, then call views.update.

There's a chance that during these calls, task A take longer to complete and updates the view incorrectly. To prevent this, there is a hash property in all block_actions payloads (sent when you use an interactive component). You can pass this hash value when calling views.update. If the hash is outdated then the API call will be rejected.


Pushing new views to a modal

As opposed to updating a modal, 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.

The new view you push will become immediately visible to the user. When the user closes or submits the new view, they’ll go back to the previous one.

Your app may only stack 3 views on a modal (including the one created with views.open). That means that you can only call views.push 2 times for an individual 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


Handling view submissions in a modal

If your view payload contains any input blocks, you must also include a submit element or an error will be thrown. After a user clicks the submit button in your app's view, you will receive a view_submission event at your app’s Request URL. This payload will contain a state object with the values of any input blocks that the user has modified.

If you want to receive state for select menus, multi-select menus, and datepickers, you must include them as an element in an input block. Only values from input blocks are included in the state object upon submission.

Within the state object, all input blocks are stored using their block_id and unique action_id. As an example, assume you called views.open with the following view payload:

"view": {
  "type": "modal",
  "callback_id": "modal-with-inputs",
  "title": {
    "type": "plain_text",
    "text": "Modal with inputs"
  },
  "blocks": [
    {
      "type": "input",
      "block_id": "multi-line",
      "label": {
        "type": "plain_text",
        "text": "Multi-line input value"
      },
      "element": {
        "type": "plain_text_input",
        "multiline": true,
        "action_id": "ml-value"
      }
    }
  ],
  "submit": {
    "type": "plain_text",
    "text": "Submit"
  }
}

View this example

After the user submits the view, your app would receive a view_submission payload that includes a state object:

{ 
  "type": "view_submission",
  "team": { ... },
  "user": { ... },
  "view": {
    "id": "VNHU13V36",
    "type": "modal",
    "title": { ... },
    "submit": { ... },
    "blocks": [ ... ],
    "private_metadata": "shhh-its-secret",
    "callback_id": "modal-with-inputs",
    "state": {
      "values": {
        "multi-line": {
          "ml-value": { 
            "type": "plain_text_input",
            "value": "Multi-line input value"
          } 
        } 
      } 
    },
    "hash": "156663117.cd33ad1f"
  }
}

An app can access the value of the input block inside of the values object (within state) in the view_submission payload. For the example above, you would access the input value by accessing state["values"]["multi-line"]["ml-value"].

When creating a view, set unique block_ids inside of the view's blocks and unique action_ids inside of each block element. This will make it easier to track than storing the generated IDs returned in response payloads.

After your app receives a view_submission event, you have 3 seconds to respond with a valid response_action. Response actions allow your app to close the current view, display errors, update the view, or clear modals depending on what your app sees fit:

Close the current view

The simplest way to respond to a view_submission event is with a 200 OK. This will close the current view and requires no value for response_action. If there are no more views left in the stack, the modal will close. Otherwise, the modal will display the next view in the stack.

Display errors in the view

Upon receiving a view_submission event, your app may want to validate the view’s inputs. If any of the inputs contain errors, like an invalid email or an empty field, your app may respond to a view_submission event with an object of errors and a response_action of errors.

Here’s an example of how an app might respond there is an error in a block with ticket-due-date as the block_id:

{
  "response_action": "errors",
  "errors": {
    "ticket-due-date": "You may not select a due date in the past"
  }
}
A modal that is rendering errors supplied by the developer

For each error, you supply a key - the block_id of the erroneous input block, and a value - the plain text error message that is displayed to the user.

The above JSON object would render the error within the modal around the ticket_due_date block, then let the user resubmit the view.

Update the current view

An app can replace the content of the view by setting the response_action to update and passing a new view payload.

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

Push a new view

An app can push a fresh new view on top of the submitted one by setting the response_action to push and passing a new payload.

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

As opposed to updating the view, this will add an entirely new view to the stack. So when a user submits or cancels the new, pushed view, they’ll go back to the previous one.

Your app can only stack three views including the initial view. This means that you can only push two additional views after you open the modal.

Close all views in the modal

If all views in the modal should be closed, set the response_action to clear. Regardless of the amount of views in the stack, the modal will close and the user will go back to the client.

{
  "response_action": "clear"
}

Passing extra information between views

When your app opens a new view or updates an existing view, it may optionally pass additional data via the private_metadata parameter. The private_metadata string may be up to 3000 characters long and is not shown to users.

The private_metadata string will never be sent to clients. It is only returned in view_submission and block_actions events.


When users dismiss a modal by clicking on the "Cancel" button or "x" in the top-right corner, Slack can send you a view_closed event if you wish. To receive the event, set the notify_on_close value to true when creating a view with views.open, views.push, or in response to a view_submission.

If the user closes an individual view using the cancel button, you will receive a view_closed event with the corresponding view ID. However, if the user exits the modal with the "x" in the top-right corner, you'll receive a single view_closed event with the root view ID and the is_cleared flag set to true.