Rendering messages

<BlockKit /> takes one required prop, data, and renders the whole payload. What follows is what data accepts, how surfaces work, and how to compose individual blocks when you don't want the all-in-one component.

Accepted input shapes

ShapeExample
Payload object{ blocks: [ … ] }
Block array[ { type: "section", … } ]
JSON string'{"blocks":[…]}' or '[…]'

A JSON string is parsed before rendering; malformed JSON throws BlockKitInvariantError with code: "invalid_json". This makes <BlockKit /> safe to point directly at editor output:

import { BlockKit } from "react-blockkit";

const jsonFromEditor = '{"blocks":[{"type":"section","text":{"type":"mrkdwn","text":"hi"}}]}';

export function Preview() {
  return <BlockKit data={jsonFromEditor} />;
}

Once the input is block-shaped, rendering is total: every entry produces visible output, including unknown or future block types. See Unknown blocks and fallbacks for the exact degradation behavior, and the validation reference for strict Slack-acceptance checks.

A fuller message

Blocks render in payload order with Slack's spacing rhythm:

Deploy summary

Service: api-gateway
Version: v2.41.0
Region: us-east-1
Status: Healthy

Triggered from main · 3 checks passed
View payload JSON
{
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "Deploy summary"
      }
    },
    {
      "type": "section",
      "fields": [
        {
          "type": "mrkdwn",
          "text": "*Service:*\napi-gateway"
        },
        {
          "type": "mrkdwn",
          "text": "*Version:*\n`v2.41.0`"
        },
        {
          "type": "mrkdwn",
          "text": "*Region:*\nus-east-1"
        },
        {
          "type": "mrkdwn",
          "text": "*Status:*\n:white_check_mark: Healthy"
        }
      ]
    },
    {
      "type": "divider"
    },
    {
      "type": "context",
      "elements": [
        {
          "type": "mrkdwn",
          "text": "Triggered from `main` · 3 checks passed"
        }
      ]
    }
  ]
}

Surfaces

Slack renders blocks in three surfaces. Pass the one you are previewing:

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

export function Preview({ data }: { data: BlockKitInput }) {
  return <BlockKit data={data} surface="modal" />;
}

surface is "message" (default), "modal", or "home". Rendering never rejects a payload because of the selected surface, so a block Slack would not accept there still renders. This payload carries an input block, which Slack accepts only in modals and home tabs, and the message surface renders it anyway:

surface="message"

Deploy summary

api-gateway v2.41.0 is live in us-east-1.
Rollback note
View payload JSON
{
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "Deploy summary"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*api-gateway* `v2.41.0` is live in `us-east-1`."
      }
    },
    {
      "type": "input",
      "label": {
        "type": "plain_text",
        "text": "Rollback note"
      },
      "element": {
        "type": "plain_text_input",
        "action_id": "note"
      }
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "action_id": "confirm",
          "style": "primary",
          "text": {
            "type": "plain_text",
            "text": "Confirm"
          }
        }
      ]
    }
  ]
}

The pixels do not change when you switch surfaces. surface feeds context and validation, never the render, so the check you want is the validator:

import { assertBlockKitData } from "react-blockkit";

// Throws BlockKitInvariantError, code "surface_mismatch", at
// data.blocks[2]: "input blocks are only legal on modal and home surfaces".
assertBlockKitData(payload, "message");

Pass the same surface to parseBlockKitData or assertBlockKitData whenever you need strict surface compatibility. That page lists every surface-restricted block.

Compose individual blocks

Every layout block and core element is exported as its own component. Wrap composed blocks in BlockKitProvider so they share the same surface, resolvers, and action handler that <BlockKit /> would provide:

import { BlockKitProvider, ButtonElement, SectionBlock } from "react-blockkit";

export function ComposedPreview() {
  return (
    <BlockKitProvider surface="message">
      <SectionBlock
        block={{
          type: "section",
          text: { type: "plain_text", text: "Composable block" },
        }}
      />
      <ButtonElement
        element={{
          type: "button",
          action_id: "continue",
          text: { type: "plain_text", text: "Continue" },
        }}
      />
    </BlockKitProvider>
  );
}

Standalone components follow the same tolerant rendering rules as <BlockKit />: block components take the Slack object as a block prop, elements take element, and unknown shapes degrade to visible fallbacks. The full component list is in the <BlockKit /> reference.

theme lives on <BlockKit />, not on BlockKitProvider, so standalone blocks default to light styling. See Theming.