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". An editor can pass its current JSON directly when the host catches that error:

import { BlockKit } from "react-blockkit";

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

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

Rendering is tolerant and does not call the strict validators. Unknown or future discriminators produce labeled fallbacks; malformed known shapes render the usable fields or a local fallback instead of rejecting the whole payload. See Unknown blocks and fallbacks for the degradation behavior, and the validation reference for strict package checks and known compatibility limitations.

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". It sets the root's data-surface attribute and provider context, and it selects the presentation Slack uses on that surface (measured in the Block Kit Builder):

  • message: small 28px controls, actions elements flowing inline, 4px between blocks.
  • modal: medium 36px controls in input blocks, small controls in a stretched two-column actions grid, 12px between blocks (4px before context and divider), and image blocks centred with the title beneath.
  • home: medium 36px controls everywhere with inline actions, and image blocks with the title beneath.

On every surface an actions block shows its first five elements and folds the rest behind a "+ N more" button. The prop does not validate or filter blocks. This message example includes an input block, which both Slack's current message surface and the renderer support:

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

Validation helpers have their own, separate surface argument:

import { assertBlockKitData } from "react-blockkit";

const modalOnly = {
  blocks: [
    {
      type: "alert",
      level: "info",
      text: { type: "plain_text", text: "Heads up" },
    },
  ],
} as const;

// Throws BlockKitInvariantError with code "surface_mismatch".
assertBlockKitData(modalOnly, "message");

Pass the intended surface directly to parseBlockKitData or assertBlockKitData when you need the package's strict surface checks. The current validator applies a modal/home-only rule to input blocks and therefore rejects them for "message" even though Slack and the renderer support them there. The validation reference documents that compatibility limitation.

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 block components take the typed Slack object as a block prop; element components take element. The dispatch components Block and InteractiveElement provide the unknown-discriminator fallbacks used by <BlockKit />. The full component and prop list is in the <BlockKit /> reference.

theme lives on <BlockKit />, not on BlockKitProvider, so standalone blocks use the stylesheet's default light variables. The global CSS import is still required. See Theming.