Skip to content

feat(model): add card, carousel, and alert Block Kit blocks - #1624

Open
srtaalej wants to merge 7 commits into
mainfrom
ale-add-card-carousel-alert-blocks
Open

feat(model): add card, carousel, and alert Block Kit blocks#1624
srtaalej wants to merge 7 commits into
mainfrom
ale-add-card-carousel-alert-blocks

Conversation

@srtaalej

@srtaalej srtaalej commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds CardBlock — structured content display (hero image, icon, title, subtitle, body, actions)
  • Adds CarouselBlock — horizontally scrollable collection of cards (1–10)
  • Adds AlertBlock — prominent notice block with severity levels (modals only)
  • Follow up to docs: add Block Kit type instructions for slack-api-model #1623
Screenshot 2026-07-22 at 3 19 01 PM Screenshot 2026-07-22 at 3 19 18 PM

Parity with node-slack-sdk#2567 and python-slack-sdk#1865.

Test plan

  • Deserialization tests in BlockKitTest.java (parse + round-trip + builder)
  • Static helper tests in BlocksTest.java
  • All 66 tests pass
Test app.java
  package blockkit;

  import com.slack.api.bolt.App;
  import com.slack.api.bolt.AppConfig;
  import com.slack.api.bolt.socket_mode.SocketModeApp;
  import com.slack.api.model.block.composition.MarkdownTextObject;
  import com.slack.api.model.block.element.ButtonElement;
  import com.slack.api.model.block.element.ImageElement;

  import java.util.Arrays;

  import static com.slack.api.model.block.Blocks.*;
  import static com.slack.api.model.block.composition.BlockCompositions.*;
  import static com.slack.api.model.view.Views.*;

  public class App {

      public static void main(String[] args) throws Exception {
          var app = new App(AppConfig.builder()
                  .singleTeamBotToken(System.getenv("SLACK_BOT_TOKEN"))
                  .build());

          // /cards — sends a carousel with cards to the channel
          app.command("/cards", (req, ctx) -> {
              ctx.client().chatPostMessage(r -> r
                      .channel(req.getPayload().getChannelId())
                      .text("Carousel and Card blocks demo")
                      .blocks(asBlocks(
                              carousel(c -> c
                                      .elements(Arrays.asList(
                                              card(card -> card
                                                      .heroImage(ImageElement.builder()

  .imageUrl("https://picsum.photos/seed/a/400/200")
                                                              .altText("Card 1 hero")
                                                              .build())
                                                      .slackIcon(slackIcon("rocket"))

  .title(MarkdownTextObject.builder().text("Launch Status").build())

  .subtitle(MarkdownTextObject.builder().text("All systems go").build())
                                                      .body(MarkdownTextObject.builder().text("The 
  deployment completed successfully.").build())
                                                      .actions(Arrays.asList(
                                                              ButtonElement.builder()
                                                                      .text(plainText("View"))
                                                                      .actionId("card_1_btn")
                                                                      .build()
                                                      ))
                                              ),
                                              card(card -> card
                                                      .heroImage(ImageElement.builder()

  .imageUrl("https://picsum.photos/seed/b/400/200")
                                                              .altText("Card 2 hero")
                                                              .build())
                                                      .icon(ImageElement.builder()

  .imageUrl("https://picsum.photos/seed/icon/32/32")
                                                              .altText("Custom icon")
                                                              .build())

  .title(MarkdownTextObject.builder().text("Custom Icon Card").build())
                                                      .body(MarkdownTextObject.builder().text("This 
  card uses a custom image icon.").build())
                                                      .actions(Arrays.asList(
                                                              ButtonElement.builder()
                                                                      .text(plainText("Open"))
                                                                      .actionId("card_2_btn")
                                                                      .build()
                                                      ))
                                              ),
                                              card(card -> card

  .title(MarkdownTextObject.builder().text("Minimal Card").build())
                                                      .body(MarkdownTextObject.builder().text("No 
  hero image, no icon, no actions.").build())
                                              )
                                      ))
                              )
                      ))
              );
              return ctx.ack();
          });

          // /alerts — opens a modal with alert blocks
          app.command("/alerts", (req, ctx) -> {
                                              ),
                                              card(card -> card
                                                      .heroImage(ImageElement.builder()

  .imageUrl("https://picsum.photos/seed/b/400/200")
                                                              .altText("Card 2 hero")
                                                              .build())
                                                      .icon(ImageElement.builder()

  .imageUrl("https://picsum.photos/seed/icon/32/32")
                                                              .altText("Custom icon")
                                                              .build())

  .title(MarkdownTextObject.builder().text("Custom Icon Card").build())
                                                      .body(MarkdownTextObject.builder().text("This
  card uses a custom image icon.").build())
                                                      .actions(Arrays.asList(
                                                              ButtonElement.builder()
                                                                      .text(plainText("Open"))
                                                                      .actionId("card_2_btn")
                                                                      .build()
                                                      ))
                                              ),
                                              card(card -> card

  .title(MarkdownTextObject.builder().text("Minimal Card").build())
                                                      .body(MarkdownTextObject.builder().text("No 
  hero image, no icon, no actions.").build())
                                              )
                                      ))
                              )
                      ))
              );
              return ctx.ack();
          });

          // /alerts — opens a modal with alert blocks
          app.command("/alerts", (req, ctx) -> {
              ctx.client().viewsOpen(r -> r
                      .triggerId(req.getPayload().getTriggerId())
                      .view(view(v -> v
                              .type("modal")
                              .title(viewTitle(vt -> vt.type("plain_text").text("Alert Block
  Demo")))
                              .close(viewClose(vc -> vc.type("plain_text").text("Close")))
                              .blocks(asBlocks(
                                      alert(a -> a.text(plainText("Default
  alert")).level("default")),
                                      alert(a -> a.text(plainText("Info alert")).level("info")),
                                      alert(a -> a.text(plainText("Warning
  alert")).level("warning")),
                                      alert(a -> a.text(plainText("Error alert")).level("error")),
                                      alert(a -> a.text(plainText("Success
  alert")).level("success"))
                              ))
                      ))
              );
              return ctx.ack();
          });

          app.blockAction("card_1_btn", (req, ctx) -> ctx.ack());
          app.blockAction("card_2_btn", (req, ctx) -> ctx.ack());

          new SocketModeApp(System.getenv("SLACK_APP_TOKEN"), app).start();
      }
  }

@srtaalej
srtaalej marked this pull request as ready for review July 22, 2026 19:21
@srtaalej
srtaalej requested a review from a team as a code owner July 22, 2026 19:21
@srtaalej srtaalej self-assigned this Jul 22, 2026
@srtaalej srtaalej added enhancement M-T: A feature request for new functionality project:slack-api-model project:slack-api-model semver:minor labels Jul 22, 2026
@srtaalej
srtaalej requested a review from WilliamBergamin July 22, 2026 19:23
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.37%. Comparing base (da4ba33) to head (ac33ff6).

Files with missing lines Patch % Lines
...api/model/block/composition/BlockCompositions.java 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1624      +/-   ##
============================================
+ Coverage     73.34%   73.37%   +0.02%     
- Complexity     4521     4531      +10     
============================================
  Files           478      478              
  Lines         14300    14308       +8     
  Branches       1490     1490              
============================================
+ Hits          10489    10499      +10     
  Misses         2920     2920              
+ Partials        891      889       -2     
Flag Coverage Δ
jdk-14 73.37% <75.00%> (+0.02%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@WilliamBergamin WilliamBergamin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is looking good 🚀

There is one blocking comment around the slackIcon type

* The name of a built-in Slack icon displayed next to the title and subtitle.
* Mutually exclusive with {@code icon}.
*/
private String slackIcon;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From these docs the type for slackIkon should be some sort of composition object with type: icon 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah yes good catch

* An array of {@link CardBlock card} blocks. Must contain between 1 and 10 cards.
*/
@Builder.Default
private List<CardBlock> elements = new ArrayList<>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason why we are initializing this value as an empty list?
Would not initializing it follow existing patterns?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmm no that was an oversight thanks for catching

Comment on lines +64 to +69
case AlertBlock.TYPE:
return AlertBlock.class;
case CardBlock.TYPE:
return CardBlock.class;
case CarouselBlock.TYPE:
return CarouselBlock.class;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice 🚀

srtaalej and others added 3 commits July 28, 2026 14:37
Follow project convention of not initializing List fields to empty
collections — null vs empty has semantic meaning in the Slack API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ckIcon

The slackIcon field on CardBlock should be a composition object with
type "icon" and a name field, not a plain String.

https://docs.slack.dev/reference/block-kit/composition-objects/slack-icon-object
@srtaalej
srtaalej requested a review from WilliamBergamin July 28, 2026 19:16
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 project:slack-api-model project:slack-api-model semver:minor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants