Add interactive UI elements to cards

This page explains how to add widgets and UI elements to cards so that users can interact with your Google Chat app, such as by clicking a button or submitting information.

Chat apps can use the following Chat interfaces to build interactive cards:

  • Messages that contain one or more cards.
  • Homepages, which is a card that appears from the Home tab in direct messages with the Chat app.
  • Dialogs, which are cards that open in a new window from messages and homepages.

When users interact with cards, Chat apps can use the data that they receive to process and respond accordingly. For details, see Collect and process information from Google Chat users.


Use the Card Builder to design and preview messaging and user interfaces for Chat apps:

Open the Card Builder

Prerequisites

A Google Chat app that's enabled for interactive features. To create an interactive Chat app, complete one of the following quickstarts based on the app architecture that you want to use:

Add a button

The ButtonList widget displays a set of buttons. Buttons can display text, an icon, or both text and an icon. Each Button supports an OnClick action that occurs when users click the button. For example:

  • Open a hyperlink with OpenLink, in order to provide users with additional information.
  • Run an action that runs a custom function, like calling an API.

For accessibility, buttons support alternative text.

Add a button that runs a custom function

The following is a card consisting of a ButtonList widget with two buttons. One button opens the Google Chat developer documentation in a new tab. The other button runs a custom function called goToView() and passes the viewType="BIRD EYE VIEW" parameter.

Add a button with Material Design style

The following displays a set of buttons in different Material Design button styles.

To apply Material Design style, don't include the 'color' attribute.

Add a button with custom color and a deactivated button

You can prevent users from clicking a button by setting "disabled": "true".

The following displays a card consisting of a ButtonList widget with two buttons. One button uses the Color field to customize the button's background color. The other button is deactivated with the Disabled field, which prevents the user from clicking the button and executing the function.

Add a button with an icon

The following displays a card consisting of a ButtonList widget with two icon Button widgets. One button uses the knownIcon field to display Google Chat's built-in email icon. The other button uses the iconUrl field to display a custom icon widget.

Add a button with an icon and text

The following displays a card consisting of a ButtonList widget that prompts the user to send an email. The first button displays an email icon and the second button displays text. The user can click either the icon or text button to run the sendEmail function.

Customize the button for a collapsible section

Customize the control button that collapses and expands sections within a card. Choose from a range of icons or images to visually represent the section's content, making it easier for users to understand and interact with the information.

Add an Overflow Menu

The Overflow menu can be used in Chat cards to offer additional options and actions. It lets you include more options without cluttering the card's interface, ensuring a clean and organized design.

Add a Chips list

The ChipList widget provides a versatile and visually appealing way to display information. Use chip lists to represent tags, categories, or other relevant data, making it easier for users to navigate and interact with your content.

Collect information from users

This section explains how you can add widgets that collect information, such as text or selections.

To learn how to process what users input, see Collect and process information from Google Chat users.

Collect text

The TextInput widget provides a field in which users can enter text. The widget supports suggestions, which help users enter uniform data, and on-change actions, which are Actions that run when a change occurs in the text input field, like a user adding or deleting text.

When you need to collect abstract or unknown data from users, use this TextInput widget. To collect defined data from users, use the SelectionInput widget instead.

The following is a card consisting of a TextInput widget:

Collect dates or times

The DateTimePicker widget lets users input a date, a time, or both a date and a time. Or, users can use the picker to select dates and times. If users input an invalid date or time, the picker shows an error that prompts users to input the information correctly.

The following displays a card consisting of three different types of DateTimePicker widgets:

Let users select items

The SelectionInput widget provides a set of selectable items, such as checkboxes, radio buttons, switches, or a drop-down menu. You can use this widget to collect defined and standardized data from users. To collect undefined data from users, use the TextInput widget instead.

The SelectionInput widget supports suggestions, which help users enter uniform data, and on-change actions, which are Actions that run when a change occurs in a selection input field, such as a user selecting or un-selecting an item.

Chat apps can receive and process the value of selected items. For details about working with form inputs, see Process information inputted by users.

This section provides examples of cards that use the SelectionInput widget. The examples use different types of section inputs:

Add a checkbox

The following displays a card that asks the user to specify whether a contact is professional, personal, or both, with a SelectionInput widget that uses checkboxes:

Add a radio button

The following displays a card that asks the user to specify whether a contact is professional or personal with a SelectionInput widget that uses radio buttons:

Add a switch

The following displays a card that asks the user to specify whether a contact is professional, personal, or both with a SelectionInput widget that uses switches:

The following displays a card that asks the user to specify whether a contact is professional or personal with a SelectionInput widget that uses a drop-down menu:

Add a multiselect menu

The following displays a card that asks the user to select contacts from a multiselect menu:

You can populate items for a multiselect menu from the following data sources in Google Workspace:

  • Google Workspace users: You can only populate users within the same Google Workspace organization.
  • Chat spaces: The user inputting items in the multiselect menu can only view and select spaces that they belong to within their Google Workspace organization.

To use Google Workspace data sources, you specify the platformDataSource field. Unlike other selection input types, you omit SectionItem objects, because these selection items are dynamically sourced from Google Workspace.

The following code shows a multiselect menu of Google Workspace users. To populate users, the selection input sets commonDataSource to USER:

JSON

{
  "selectionInput": {
    "name": "contacts",
    "type": "MULTI_SELECT",
    "label": "Selected contacts",
    "multiSelectMaxSelectedItems": 5,
    "multiSelectMinQueryLength": 1,
    "platformDataSource": {
      "commonDataSource": "USER"
    }
  }
}

The following code shows a multiselect menu of Chat spaces. To populate spaces, the selection input specifies the hostAppDataSource field. The multiselect menu also sets defaultToCurrentSpace to true, which makes the current space the default selection in the menu:

JSON

{
  "selectionInput": {
    "name": "spaces",
    "type": "MULTI_SELECT",
    "label": "Selected contacts",
    "multiSelectMaxSelectedItems": 3,
    "multiSelectMinQueryLength": 1,
    "platformDataSource": {
      "hostAppDataSource": {
        "chatDataSource": {
          "spaceDataSource": {
            "defaultToCurrentSpace": true
          }
        }
      }
    }
  }
}

Multiselect menus can also populate items from a third-party or external data source. For example, you can use multiselect menus to help a user select from a list of sales leads from a customer relationship management (CRM) system.

To use an external data source, you use the externalDataSource field to specify a function that returns items from the data source.

To reduce the requests to an external data source, you can include suggested items that appear in the multiselect menu before users type in the menu. For example, you can populate recently searched contacts for the user. To populate suggested items from an external data source, specify SelectionItem objects.

The following code shows a multiselect menu of items from an external set of contacts for the user. The menu displays one contact by default and runs the function getContacts to retrieve and populate items from the external data source:

Node.js

node/selection-input/index.js
selectionInput: {
  name: "contacts",
  type: "MULTI_SELECT",
  label: "Selected contacts",
  multiSelectMaxSelectedItems: 3,
  multiSelectMinQueryLength: 1,
  externalDataSource: { function: "getContacts" },
  // Suggested items loaded by default.
  // The list is static here but it could be dynamic.
  items: [getContact("3")]
}

Python

python/selection-input/main.py
'selectionInput': {
  'name': "contacts",
  'type': "MULTI_SELECT",
  'label': "Selected contacts",
  'multiSelectMaxSelectedItems': 3,
  'multiSelectMinQueryLength': 1,
  'externalDataSource': { 'function': "getContacts" },
  # Suggested items loaded by default.
  # The list is static here but it could be dynamic.
  'items': [get_contact("3")]
}

Java

java/selection-input/src/main/java/com/google/chat/selectionInput/App.java
.setSelectionInput(new GoogleAppsCardV1SelectionInput()
  .setName("contacts")
  .setType("MULTI_SELECT")
  .setLabel("Selected contacts")
  .setMultiSelectMaxSelectedItems(3)
  .setMultiSelectMinQueryLength(1)
  .setExternalDataSource(new GoogleAppsCardV1Action().setFunction("getContacts"))
  .setItems(List.of(getContact("3")))))))))));

Apps Script

apps-script/selection-input/selection-input.gs
selectionInput: {
  name: "contacts",
  type: "MULTI_SELECT",
  label: "Selected contacts",
  multiSelectMaxSelectedItems: 3,
  multiSelectMinQueryLength: 1,
  externalDataSource: { function: "getContacts" },
  // Suggested items loaded by default.
  // The list is static here but it could be dynamic.
  items: [getContact("3")]
}

For external data sources, you can also autocomplete items that users start typing in the multiselect menu. For example, if a user starts typing Atl for a menu that populates cities in the United States, your Chat app can autosuggest Atlanta before the user finishes typing. You can autocomplete up to 100 items.

To autocomplete items, you create a function that queries the external data source and returns items whenever a user types in the multiselect menu. The function must do the following:

  • Pass an event object that represents the user interaction with the menu.
  • Identify that the interaction event's invokedFunction value matches the function from the externalDataSource field.
  • When the functions match, return suggested items from the external data source. To suggest items based on what the user types, get the value for the autocomplete_widget_query key. This value represents what the user types in the menu.

The following code autocompletes items from an external data resource. Using the previous example, the Chat app suggests items based on when the function getContacts is triggered:

Node.js

node/selection-input/index.js
/**
 * Responds to a WIDGET_UPDATE event in Google Chat.
 *
 * @param {Object} event The event object from Chat API.
 * @return {Object} Response from the Chat app.
 */
function onWidgetUpdate(event) {
  if (event.common["invokedFunction"] === "getContacts") {
    const query = event.common.parameters["autocomplete_widget_query"];
    return { actionResponse: {
      type: "UPDATE_WIDGET",
      updatedWidget: { suggestions: { items: [
        // The list is static here but it could be dynamic.
        getContact("1"), getContact("2"), getContact("3"), getContact("4"), getContact("5")
      // Only return items based on the query from the user
      ].filter(e => !query || e.text.includes(query))}}
    }};
  }
}

/**
 * Generate a suggested contact given an ID.
 *
 * @param {String} id The ID of the contact to return.
 * @return {Object} The contact formatted as a suggested item for selectors.
 */
function getContact(id) {
  return {
    value: id,
    startIconUri: "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
    text: "Contact " + id
  };
}

Python

python/selection-input/main.py
def on_widget_update(event: dict) -> dict:
  """Responds to a WIDGET_UPDATE event in Google Chat."""
  if "getContacts" == event.get("common").get("invokedFunction"):
    query = event.get("common").get("parameters").get("autocomplete_widget_query")
    return { 'actionResponse': {
      'type': "UPDATE_WIDGET",
      'updatedWidget': { 'suggestions': { 'items': list(filter(lambda e: query is None or query in e["text"], [
        # The list is static here but it could be dynamic.
        get_contact("1"), get_contact("2"), get_contact("3"), get_contact("4"), get_contact("5")
      # Only return items based on the query from the user
      ]))}}
    }}


def get_contact(id: str) -> dict:
  """Generate a suggested contact given an ID."""
  return {
    'value': id,
    'startIconUri': "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
    'text': "Contact " + id
  }

Java

java/selection-input/src/main/java/com/google/chat/selectionInput/App.java
// Responds to a WIDGET_UPDATE event in Google Chat.
Message onWidgetUpdate(JsonNode event) {
  if ("getContacts".equals(event.at("/invokedFunction").asText())) {
    String query = event.at("/common/parameters/autocomplete_widget_query").asText();
    return new Message().setActionResponse(new ActionResponse()
      .setType("UPDATE_WIDGET")
      .setUpdatedWidget(new UpdatedWidget()
        .setSuggestions(new SelectionItems().setItems(List.of(
          // The list is static here but it could be dynamic.
          getContact("1"), getContact("2"), getContact("3"), getContact("4"), getContact("5")
        // Only return items based on the query from the user
        ).stream().filter(e -> query == null || e.getText().indexOf(query) > -1).toList()))));
  }
  return null;
}

// Generate a suggested contact given an ID.
GoogleAppsCardV1SelectionItem getContact(String id) {
  return new GoogleAppsCardV1SelectionItem()
    .setValue(id)
    .setStartIconUri("https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png")
    .setText("Contact " + id);
}

Apps Script

apps-script/selection-input/selection-input.gs
/**
 * Responds to a WIDGET_UPDATE event in Google Chat.
 *
 * @param {Object} event The event object from Chat API.
 * @return {Object} Response from the Chat app.
 */
function onWidgetUpdate(event) {
  if (event.common["invokedFunction"] === "getContacts") {
    const query = event.common.parameters["autocomplete_widget_query"];
    return { actionResponse: {
      type: "UPDATE_WIDGET",
      updatedWidget: { suggestions: { items: [
        // The list is static here but it could be dynamic.
        getContact("1"), getContact("2"), getContact("3"), getContact("4"), getContact("5")
      // Only return items based on the query from the user
      ].filter(e => !query || e.text.includes(query))}}
    }};
  }
}

/**
 * Generate a suggested contact given an ID.
 *
 * @param {String} id The ID of the contact to return.
 * @return {Object} The contact formatted as a suggested item for selectors.
 */
function getContact(id) {
  return {
    value: id,
    startIconUri: "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
    text: "Contact " + id
  };
}

Validate data inputted to cards

This page explains how to validate data inputted to a card's action and widgets. For example, you can validate that a text input field has text entered by the user, or that it contains a certain number of characters.

Set required widgets for actions

As part of the card's action, add names of widgets that an action needs to its requiredWidgets list.

If any widgets listed here don't have a value when this action is invoked, then the form action submission is cancelled.

When "all_widgets_are_required": "true" is set for an action, then all widgets in the card are required by this action.

Set an all_widgets_are_required action in multiselect

JSON

{
  "sections": [
    {
      "header": "Select contacts",
      "widgets": [
        {
          "selectionInput": {
            "type": "MULTI_SELECT",
            "label": "Selected contacts",
            "name": "contacts",
            "multiSelectMaxSelectedItems": 3,
            "multiSelectMinQueryLength": 1,
            "onChangeAction": {
              "all_widgets_are_required": true
            },
            "items": [
              {
                "value": "contact-1",
                "startIconUri": "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
                "text": "Contact 1",
                "bottomText": "Contact one description",
                "selected": false
              },
              {
                "value": "contact-2",
                "startIconUri": "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
                "text": "Contact 2",
                "bottomText": "Contact two description",
                "selected": false
              },
              {
                "value": "contact-3",
                "startIconUri": "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
                "text": "Contact 3",
                "bottomText": "Contact three description",
                "selected": false
              },
              {
                "value": "contact-4",
                "startIconUri": "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
                "text": "Contact 4",
                "bottomText": "Contact four description",
                "selected": false
              },
              {
                "value": "contact-5",
                "startIconUri": "https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png",
                "text": "Contact 5",
                "bottomText": "Contact five description",
                "selected": false
              }
            ]
          }
        }
      ]
    }
  ]
}
Set an all_widgets_are_required action in dateTimePicker

JSON

{
  "sections": [
    {
      "widgets": [
        {
          "textParagraph": {
            "text": "A datetime picker widget with both date and time:"
          }
        },
        {
          "divider": {}
        },
        {
          "dateTimePicker": {
            "name": "date_time_picker_date_and_time",
            "label": "meeting",
            "type": "DATE_AND_TIME"
          }
        },
        {
          "textParagraph": {
            "text": "A datetime picker widget with just date:"
          }
        },
        {
          "divider": {}
        },
        {
          "dateTimePicker": {
            "name": "date_time_picker_date_only",
            "label": "Choose a date",
            "type": "DATE_ONLY",
            "onChangeAction":{
              "all_widgets_are_required": true
            }
          }
        },
        {
          "textParagraph": {
            "text": "A datetime picker widget with just time:"
          }
        },
        {
          "divider": {}
        },
        {
          "dateTimePicker": {
            "name": "date_time_picker_time_only",
            "label": "Select a time",
            "type": "TIME_ONLY"
          }
        }
      ]
    }
  ]
}
Set an all_widgets_are_required action in drop-down menu

JSON

{
  "sections": [
    {
      "header": "Section Header",
      "collapsible": true,
      "uncollapsibleWidgetsCount": 1,
      "widgets": [
        {
          "selectionInput": {
            "name": "location",
            "label": "Select Color",
            "type": "DROPDOWN",
            "onChangeAction": {
              "all_widgets_are_required": true
            },
            "items": [
              {
                "text": "Red",
                "value": "red",
                "selected": false
              },
              {
                "text": "Green",
                "value": "green",
                "selected": false
              },
              {
                "text": "White",
                "value": "white",
                "selected": false
              },
              {
                "text": "Blue",
                "value": "blue",
                "selected": false
              },
              {
                "text": "Black",
                "value": "black",
                "selected": false
              }
            ]
          }
        }
      ]
    }
  ]
}

Set the validation for a text input widget

In the textInput widget's validation field, it can specify the character limit and input type for this text input widget.

Set a character limit for a text input widget

JSON

{
  "sections": [
    {
      "header": "Tell us about yourself",
      "collapsible": true,
      "uncollapsibleWidgetsCount": 2,
      "widgets": [
        {
          "textInput": {
            "name": "favoriteColor",
            "label": "Favorite color",
            "type": "SINGLE_LINE",
            "validation": {"character_limit":15},
            "onChangeAction":{
              "all_widgets_are_required": true
            }
          }
        }
      ]
    }
  ]
}
Set the input type for a text input widget

JSON

{
  "sections": [
    {
      "header": "Validate text inputs by input types",
      "collapsible": true,
      "uncollapsibleWidgetsCount": 2,
      "widgets": [
        {
          "textInput": {
            "name": "mailing_address",
            "label": "Please enter a valid email address",
            "type": "SINGLE_LINE",
            "validation": {
              "input_type": "EMAIL"
            },
            "onChangeAction": {
              "all_widgets_are_required": true
            }
          }
        },
        {
          "textInput": {
            "name": "validate_integer",
            "label": "Please enter a number",
              "type": "SINGLE_LINE",
            "validation": {
              "input_type": "INTEGER"
            }
          }
        },
        {
          "textInput": {
            "name": "validate_float",
            "label": "Please enter a number with a decimal",
            "type": "SINGLE_LINE",
            "validation": {
              "input_type": "FLOAT"
            }
          }
        }
      ]
    }
  ]
}

Troubleshoot

When a Google Chat app or card returns an error, the Chat interface surfaces a message saying "Something went wrong." or "Unable to process your request." Sometimes the Chat UI doesn't display any error message, but the Chat app or card produces an unexpected result; for example, a card message might not appear.

Although an error message might not display in the Chat UI, descriptive error messages and log data are available to help you fix errors when error logging for Chat apps is turned on. For help viewing, debugging, and fixing errors, see Troubleshoot and fix Google Chat errors.