UI Components
Block Kit vs Adaptive Cards
| Slack Block Kit | Adaptive Card Element | Notes |
|---|---|---|
section (text) | TextBlock | Set wrap: true; convert *bold* mrkdwn → **bold** Markdown |
section (fields) | FactSet | Each field becomes \{ title, value \} |
section (text + accessory) | ColumnSet with 2 Columns | Col 1 = text, Col 2 = accessory |
header | TextBlock size Large, weight Bolder | |
actions | ActionSet | Max 6 actions in Teams (vs 25 in Slack) |
divider | TextBlock with separator: true | |
image | Image | alt_text (underscore) → altText (camelCase) |
context | TextBlock size Small, isSubtle: true | |
input (plain_text) | Input.Text | |
input (static_select) | Input.ChoiceSet style: "compact" | |
input (multi_select) | Input.ChoiceSet isMultiSelect: true | |
input (datepicker) | Input.Date | |
input (timepicker) | Input.Time | |
input (checkboxes) | Input.ChoiceSet style: "expanded", isMultiSelect: true | |
input (radio_buttons) | Input.ChoiceSet style: "expanded" | |
rich_text | RichTextBlock | Schema 1.5+ |
overflow menu | No equivalent | Redesign as ActionSet or Input.ChoiceSet dropdown |
Rating: GREEN for most elements, RED for overflow menus.
Markdown Differences
| Formatting | Slack mrkdwn | Adaptive Card Markdown |
|---|---|---|
| Bold | *bold* | **bold** |
| Italic | _italic_ | _italic_ |
| Strikethrough | ~strike~ | ~~strike~~ |
| Code | `code` | `code` |
| Emoji | :emoji_shortcode: | Unicode characters only |
| User mention | <@U12345> | Display name (no mention syntax) |
Impact: Failing to convert *bold* to **bold** produces literal asterisks in Teams. Slack emoji shortcodes render as plain text in Adaptive Cards.
Mitigation: Apply a text transform function when converting between formats:
// Slack mrkdwn → Adaptive Card Markdown
text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "**$1**")
.replace(/~([^~]+)~/g, "~~$1~~");
// Adaptive Card Markdown → Slack mrkdwn
text.replace(/\*\*([^*]+)\*\*/g, "*$1*")
.replace(/~~([^~]+)~~/g, "~$1~");Button / Action Styles
| Slack | Adaptive Card | Visual |
|---|---|---|
"primary" | "positive" | Green |
"danger" | "destructive" | Red |
| (default) | "default" | Neutral |
Interaction Model Difference
This is the biggest behavioral shift between platforms:
| Aspect | Slack | Teams |
|---|---|---|
| Model | Event-per-interaction — every select/button fires immediately | Form-then-submit — inputs collect, submit sends all at once |
| Select behavior | static_select fires block_actions on selection | Input.ChoiceSet does nothing until Action.Submit clicked |
| Live updates | dispatch_action: true for real-time block_actions | No equivalent — use Action.Execute with refresh for critical cases |
| Form data | Per-element: action.selected_option.value | Batched: activity.value contains all input IDs |
Impact: Bots that rely on instant-fire selects to update UI dynamically will feel different in Teams. The Teams UX has fewer round trips but less reactivity.
Mitigation: Accept the batch-submit model for Teams. Group related inputs and submit together. For cases requiring per-interaction updates, use Action.Execute with the refresh property (schema 1.4+).
Block / Action Limits
| Limit | Slack | Teams |
|---|---|---|
| Blocks per message | 50 | No formal block limit, but 28 KB payload max |
| Blocks per modal/view | 100 | 28 KB payload max |
| Actions per block | 25 per actions block | 6 per ActionSet |
| Card payload size | No formal limit | 28 KB after JSON serialization |
Mitigation: Paginate dense action rows into multiple cards. Consolidate overflow menus into Input.ChoiceSet dropdowns.
Modals vs Dialogs (Task Modules)
| Aspect | Slack Modal | Teams Dialog (Task Module) |
|---|---|---|
| Open | views.open(trigger_id, view) | Return card from dialog.open handler |
| Submit handler | app.view("callback_id") | app.on("dialog.submit") |
| Form data location | view.state.values[block_id][action_id] | activity.value.data (flat object keyed by input id) |
| Stacking | views.push() — up to 3 levels | Not supported |
| Cancel notification | notify_on_close: true → view_closed | Not supported |
| Mid-form updates | dispatch_action + block_actions + views.update | Not supported |
| Field validation | ack(\{ response_action: "errors", errors \}) | Client-side only (isRequired, regex) |
| Private metadata | private_metadata (3000 char limit) | Hidden fields in Action.Submit.data |
| View hash (race protection) | view_hash parameter in views.update | Not supported — manual _version field needed |
Rating: YELLOW for basic modal-to-dialog, RED for stacking, cancel notifications, and mid-form updates.
Mitigation Strategies
| Gap | Strategy | Effort |
|---|---|---|
| No stacking | Flatten into single dialog with step routing in submit handler. Include a "Back" button that re-renders the previous step. | 8–16 hrs |
| No cancel notification | Add explicit "Cancel" button inside the dialog. Implement 5-min timeout-based cleanup for stale locks. | 4–8 hrs |
| No mid-form updates | Multi-step dialogs for dependent fields. Action.ToggleVisibility for simple show/hide. | 8–16 hrs |
| No server-side validation | Re-open dialog with pre-populated data + error messages in field labels. Use client-side isRequired/regex where possible. | 4–8 hrs |
| No view hash | Inject _version counter into Action.Submit.data. Reject stale updates server-side. | 2–4 hrs |
Reverse Direction (Teams → Slack)
Dialogs map to modals via views.open(). Teams' batch-submit model decomposes into Slack's per-element handlers. Client-side validation (isRequired, regex) becomes server-side validation in view_submission.
App Home vs Personal Tab
| Aspect | Slack App Home | Teams Personal Tab |
|---|---|---|
| Render | views.publish() with Block Kit | tab.fetch handler returns Adaptive Card, or staticTabs in manifest for web |
| Trigger | app_home_opened event | tab.fetch fires on every tab open |
| Dynamic updates | views.publish() any time | tab.submit for interactions within tab |
| Race protection | view_hash parameter | Not supported |
Rating: YELLOW — functional equivalent exists, different event model.
Mitigation Strategies
| Strategy | How | Effort |
|---|---|---|
tab.fetch handler (Recommended) | Return Adaptive Card on every tab open. Closest to app_home_opened. | 4–8 hrs |
| Welcome message only | Send card to 1:1 chat on install.add. Simple but fires once. | 1–2 hrs |
| Static web tab | Full web page in iframe. Richer UI but needs hosting. | 8–16 hrs |