Handling actions

Buttons, selects, overflow menus, checkboxes, radio buttons, inputs, and the context_actions elements all report user interaction through a single onAction callback:

import { BlockKit, type BlockKitAction, type BlockKitInput } from "react-blockkit";

export function Preview({ data }: { data: BlockKitInput }) {
  return (
    <BlockKit
      data={data}
      onAction={(action: BlockKitAction) => {
        console.log(action.actionId, action.value);
      }}
    />
  );
}

BlockKitAction is react-blockkit's own local event shape, not the interactivity payload Slack POSTs to an app. Nothing is sent to Slack. onAction hands you a normalized preview interaction; your app decides whether to update local state or call its own backend. The callback receives no DOM event and cannot cancel a URL button's navigation.

react-blockkit calls onAction from a React event handler and does not catch or translate callback failures. React and the browser host decide how an uncaught event-handler error is reported; it does not become a BlockKitInvariantError. Handle synchronous errors and rejected async work inside your callback when the application needs controlled recovery.

The BlockKitAction shape

FieldTypeMeaning
actionIdstringThe element's action_id
typestringThe element's type ("button", "static_select", …)
blockIdstring | undefinedThe enclosing block's block_id, when set
valuestring | undefinedSingle value (button value, selected option, input text)
valuesreadonly string[] | undefinedMultiple values (multi-selects, checked checkboxes)

Elements without an action_id never emit, so the callback fires only when there is an ID to report. Optional fields are omitted from the action object rather than set to undefined.

What each element emits

ElementFires onPayload
ButtonClickvalue when the button declares one; URL buttons render as links and still emit
Overflow menuClickactionId and type only; the ellipsis button opens no menu, so no option is reported
Select (all variants)Selection changevalue, or values for multi_* variants
CheckboxesTogglevalues: every currently checked option
Radio buttonsSelectionvalue of the chosen option
Text/email/URL/number inputsEach changevalue: the current text
Rich text inputEach changevalue: the editor's plain text
File inputSelectionvalue: the first selected file's name
Date pickerClickvalue: the initial_date, or an empty string (renders as a button)
Time and datetime pickersEach changevalue: the control's current value
Icon buttonClickvalue when the element declares one
Feedback buttonsClickvalue from the pressed nested button's value; accessible name from its text.text

These controls are preview approximations. Overflow does not open a menu, external and workspace-backed selects do not fetch options, datepicker emits its initial value on click, and workflow_button is display-only. The renderer also does not reproduce Slack focus management or keyboard shortcuts. See Accessibility when the preview itself is user-facing.

For feedback controls, the positive button is named from positive_button.text.text and the negative button from negative_button.text.text. The parent does not supply those two labels.

Try it

Interact with the message below; the local BlockKitAction each element emits appears under the render.

PR #482 is ready for review.
Interact with the message above. The BlockKitAction appears here.
View payload JSON
{
  "blocks": [
    {
      "type": "section",
      "block_id": "review",
      "text": {
        "type": "mrkdwn",
        "text": "*PR #482* is ready for review."
      },
      "accessory": {
        "type": "static_select",
        "action_id": "assign_reviewer",
        "placeholder": {
          "type": "plain_text",
          "text": "Assign reviewer"
        },
        "options": [
          {
            "text": {
              "type": "plain_text",
              "text": "Maya"
            },
            "value": "maya"
          },
          {
            "text": {
              "type": "plain_text",
              "text": "Sam"
            },
            "value": "sam"
          }
        ]
      }
    },
    {
      "type": "actions",
      "block_id": "decision",
      "elements": [
        {
          "type": "button",
          "action_id": "approve",
          "text": {
            "type": "plain_text",
            "text": "Approve"
          },
          "style": "primary",
          "value": "pr-482"
        },
        {
          "type": "button",
          "action_id": "request_changes",
          "text": {
            "type": "plain_text",
            "text": "Request changes"
          }
        }
      ]
    }
  ]
}

Composed blocks

When rendering blocks individually, put onAction on BlockKitProvider, the same context <BlockKit /> uses internally:

import {
  ActionsBlock,
  BlockKitProvider,
  type ActionsBlockData,
  type BlockKitAction,
} from "react-blockkit";

export function record(action: BlockKitAction) {
  console.log(action);
}

export function Preview({ actionsBlock }: { actionsBlock: ActionsBlockData }) {
  return (
    <BlockKitProvider onAction={record}>
      <ActionsBlock block={actionsBlock} />
    </BlockKitProvider>
  );
}