Skip to content

feat(types): add ContainerBlock for block kit - #2721

Open
srtaalej wants to merge 6 commits into
mainfrom
ale-add-containerblock
Open

feat(types): add ContainerBlock for block kit#2721
srtaalej wants to merge 6 commits into
mainfrom
ale-add-containerblock

Conversation

@srtaalej

@srtaalej srtaalej commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds ContainerBlock (type: "container") to @slack/types per https://docs.slack.dev/reference/block-kit/blocks/container-block/

  • New ContainerBlock interface with all documented properties: title, rich_text_title, subtitle, child_blocks, width, icon, is_collapsible, default_collapsed, has_header_divider
  • New ContainerBlockChildBlock helper union type for the 11 supported child block types
  • Added to the KnownBlock discriminated union
  • Type tests covering happy path, sad path, and KnownBlock assignability
  • Note: the API rejects has_header_divider: true when is_collapsible: true — documented in JSDoc
Example Bolt test app (app.js)

Register a /container-block slash command in your test app and use the text argument to test different property combinations:

  • /container-block — base (title + child_blocks)
  • /container-block width — adds width: "wide"
  • /container-block mrkdwn-subtitle — subtitle with mrkdwn type
  • /container-block has-header-divider — adds has_header_divider: true
  • /container-block default-collapsed — adds is_collapsible + default_collapsed
  • /container-block icon — adds icon image element
  • /container-block rich-text-title — uses rich_text_title instead of title
  • /container-block all-collapsible — all props with collapsible
  • /container-block all-divider — all props with header divider
  • /container-block all — all compatible properties together
import { App, LogLevel } from '@slack/bolt';
import { config } from 'dotenv';

config();

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
  logLevel: LogLevel.DEBUG,
});

app.command('/container-block', async ({ ack, respond, command }) => {
  await ack();
  const variant = (command.text || '').trim();

  const base = {
    type: 'container',
    title: { type: 'plain_text', text: 'Container Block Test' },
    child_blocks: [
      { type: 'section', text: { type: 'mrkdwn', text: 'Section inside a container.' } },
      { type: 'divider' },
      { type: 'section', text: { type: 'mrkdwn', text: 'Another section below the divider.' } },
    ],
  };

  if (variant === 'width') {
    base.width = 'wide';
  } else if (variant === 'mrkdwn-subtitle') {
    base.subtitle = { type: 'mrkdwn', text: '*Bold* subtitle' };
  } else if (variant === 'has-header-divider') {
    base.has_header_divider = true;
  } else if (variant === 'default-collapsed') {
    base.is_collapsible = true;
    base.default_collapsed = true;
  } else if (variant === 'icon') {
    base.icon = {
      type: 'image',
      image_url: 'https://api.slack.com/img/blocks/bkb_template_images/plants.png',
      alt_text: 'icon',
    };
  } else if (variant === 'rich-text-title') {
    delete base.title;
    base.rich_text_title = {
      type: 'rich_text',
      elements: [
        { type: 'rich_text_section', elements: [{ type: 'text', text: 'Rich Title' }] },
      ],
    };
  } else if (variant === 'all-collapsible') {
    base.subtitle = { type: 'plain_text', text: 'Collapsible variant' };
    base.width = 'wide';
    base.icon = {
      type: 'image',
      image_url: 'https://api.slack.com/img/blocks/bkb_template_images/plants.png',
      alt_text: 'icon',
    };
    base.is_collapsible = true;
  } else if (variant === 'all-divider') {
    base.subtitle = { type: 'plain_text', text: 'Header divider variant' };
    base.width = 'wide';
    base.icon = {
      type: 'image',
      image_url: 'https://api.slack.com/img/blocks/bkb_template_images/plants.png',
      alt_text: 'icon',
    };
    base.has_header_divider = true;
  } else if (variant === 'all') {
    base.subtitle = { type: 'plain_text', text: 'All compatible properties' };
    base.width = 'wide';
    base.icon = {
      type: 'image',
      image_url: 'https://api.slack.com/img/blocks/bkb_template_images/plants.png',
      alt_text: 'icon',
    };
    base.is_collapsible = true;
  } else {
    base.subtitle = {
      type: 'plain_text',
      text: 'Base test (try: width, mrkdwn-subtitle, has-header-divider, default-collapsed, icon, rich-text-title, all)',
    };
  }

  await respond({ blocks: [base] });
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app is running!');
})();

Requirements

@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d61f4e9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@slack/types Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.12%. Comparing base (e0fa745) to head (d61f4e9).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2721   +/-   ##
=======================================
  Coverage   89.12%   89.12%           
=======================================
  Files          65       65           
  Lines       10441    10441           
  Branches      482      482           
=======================================
  Hits         9306     9306           
  Misses       1103     1103           
  Partials       32       32           
Flag Coverage Δ
cli-hooks 89.12% <ø> (ø)
cli-test 89.12% <ø> (ø)
logger 89.12% <ø> (ø)
oauth 89.12% <ø> (ø)
socket-mode 89.12% <ø> (ø)
web-api 89.12% <ø> (ø)
webhook 89.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@srtaalej srtaalej self-assigned this Aug 31, 2026
@srtaalej srtaalej added semver:minor enhancement M-T: A feature request for new functionality pkg:types applies to `@slack/types` labels Aug 31, 2026
@srtaalej srtaalej added this to the types@next milestone Aug 31, 2026
@srtaalej
srtaalej marked this pull request as ready for review August 31, 2026 18:59
@srtaalej
srtaalej requested a review from a team as a code owner August 31, 2026 18:59

@zimeg zimeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srtaalej A few comments now but this is looking good 🌠

I'm curious about inlining possible options as an enum instead of exported type. Also updating reference documentation might nice if it's showing unexpected argument types. And I also think the @jsdoc should match reference as much as possible in hopes that we can keep this synced.

Will test against slack-samples/bolt-js-examples#218 before a final review! 🌚 ✨

Comment thread packages/types/src/block-kit/blocks.ts Outdated
Comment on lines +188 to +203
/**
* A helper union type of all Block types that can be used as child blocks in a {@link ContainerBlock}.
* @see {@link https://docs.slack.dev/reference/block-kit/blocks/container-block Container block reference}.
*/
export type ContainerBlockChildBlock =
| ActionsBlock
| ContextBlock
| DividerBlock
| FileBlock
| HeaderBlock
| ImageBlock
| InputBlock
| RichTextBlock
| SectionBlock
| TableBlock
| VideoBlock;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪓 suggestion: Can we inline these options? Or perhaps avoid exporting it? I'm unsure it's used elsewhere and don't want to add additional exports without an immediate use.

Comment on lines +215 to +224
/**
* @description Plain text title for the container. Maximum length is 150 characters.
* One of `title` or `rich_text_title` is required.
*/
title?: PlainTextElement;
/**
* @description Rich text title for the container. Takes precedence over `title` if both are provided.
* One of `title` or `rich_text_title` is required.
*/
rich_text_title?: RichTextBlock;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔭 suggestion: We should update reference pages to match these types if it's correct?

Comment on lines +233 to +236
/**
* @description Controls the width of the container. Defaults to `"standard"`.
*/
width?: 'narrow' | 'standard' | 'wide' | 'full';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌟 praise: I think we should embrace enum more in ongoing iteration and expand it as needed. IIRC this hasn't been our practice forever but it offers a better experience in current releases with fast fixes onward as needed. Changing a string to enum is more difficult I fear...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement M-T: A feature request for new functionality pkg:types applies to `@slack/types` semver:minor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants