Book-style knowledge base for the
slack-blocks-to-jsxReact library. Use the index below to jump to the section you need — do not read linearly. When code or architecture changes, update the relevant chapter here in the same PR.
| Ch. | Section | Topic |
|---|---|---|
| 0 | §0 | How to use this knowledge base |
| 0 | §0.1 | What this document is |
| 0 | §0.2 | How to read it effectively |
| 0 | §0.3 | When to update it |
| 0 | §0.4 | How to update it |
| 0 | §0.5 | Style rules |
| 0 | §0.6 | Who maintains this |
| 1 | §1 | Overview, identity, links |
| 1 | §1.1 | Package identity |
| 1 | §1.2 | What it does |
| 1 | §1.3 | Repo and publish links |
| 2 | §2 | Installation & runtime requirements |
| 2 | §2.1 | Install command |
| 2 | §2.2 | Peer dependencies |
| 2 | §2.3 | Runtime dependencies |
| 2 | §2.4 | CSS import requirement |
| 3 | §3 | Public API |
| 3 | §3.1 | Exports from src/index.ts |
| 3 | §3.2 | Message component props |
| 3 | §3.3 | Header component |
| 3 | §3.4 | BlockWrapper component |
| 3 | §3.5 | Component dispatcher |
| 4 | §4 | Blocks reference |
| 4 | §4.1 | Layout & structure blocks (section, divider, header) |
| 4 | §4.2 | Media blocks (image, video, file) |
| 4 | §4.3 | Interactive blocks (actions, input, context_actions) |
| 4 | §4.4 | Content blocks (context, table, markdown) |
| 4 | §4.5 | AI/workflow blocks (plan, task_card) |
| 4 | §4.6 | Status & rich-container blocks (alert, card, carousel) |
| 4 | §4.7 | Rich text block (deep dive) |
| 5 | §5 | Elements reference |
| 5 | §5.1 | Button family (button, workflow_button) |
| 5 | §5.2 | Single-select menus |
| 5 | §5.3 | Multi-select menus |
| 5 | §5.4 | Choice elements (radio_buttons, checkboxes) |
| 5 | §5.5 | Text & input elements |
| 5 | §5.6 | Date & time pickers |
| 5 | §5.7 | File upload & rich text input |
| 5 | §5.8 | Display elements (image, overflow, feedback, icon, url) |
| 6 | §6 | Type system |
| 6 | §6.1 | types/layout.ts — block union |
| 6 | §6.2 | types/elements.ts — element union |
| 6 | §6.3 | types/objects.ts — composition objects |
| 6 | §6.4 | types/rich_text_element.ts |
| 7 | §7 | State & render hooks |
| 7 | §7.1 | data prop |
| 7 | §7.2 | hooks prop |
| 7 | §7.3 | useGlobalData internal hook |
| 8 | §8 | Rendering pipeline |
| 8 | §8.1 | Dispatcher flow |
| 8 | §8.2 | Markdown parser (Yozora + custom Slack tokenizers) |
| 8 | §8.3 | Emoji parser |
| 8 | §8.4 | Composition objects (text, confirm_dialog) |
| 9 | §9 | Styling |
| 9 | §9.1 | Tailwind scope |
| 9 | §9.2 | Dark mode |
| 9 | §9.3 | CSS build pipeline |
| 10 | §10 | Build & distribution |
| 10 | §10.1 | tsup config |
| 10 | §10.2 | tsconfig |
| 10 | §10.3 | npm scripts |
| 10 | §10.4 | dist/ output |
| 10 | §10.5 | npm publish hygiene |
| 11 | §11 | Release process |
| 12 | §12 | Test fixtures |
| 13 | §13 | Complete file tree |
| 14 | §14 | Architectural conventions |
| 15 | §15 | Playground (contributor-only, not in npm package) |
| 15 | §15.1 | What it is |
| 15 | §15.2 | Folder layout |
| 15 | §15.3 | How HMR works |
| 15 | §15.4 | Running it |
| 15 | §15.5 | How it is excluded from npm |
| 15 | §15.6 | Adding a new fixture |
| 16 | §16 | Change log of this doc |
A chapter-indexed field manual covering the entire slack-blocks-to-jsx codebase — architecture, public API, every block and element, the type system, the rendering pipeline, build tooling, and release process. It is not a tutorial and not the source of truth (the code is). It is the fastest way to answer "where does X live?" and "how does X work?" without re-reading source.
Think of it as a reference book, not a textbook. Look up, don't read through.
- Always start at the TOC. Never scroll the body looking for a topic — use the index.
- Jump by anchor link. Every TOC entry is clickable and drops you at the exact section.
- Read the smallest unit needed. A section is written to stand on its own; you rarely need the surrounding chapter.
- Follow the cross-references.
see §4.7means "jump to Chapter 4, Section 7" — do that instead of searching again. - Gap = signal. If the TOC doesn't answer your question, the doc is stale. Fix it after you learn the answer from the code (see §0.3–§0.4).
Quick routing by task:
| If you're asking… | Go to |
|---|---|
| What file is X in? | Ch. 13 (file tree) |
| What does the public API export? | Ch. 3 |
| How is block/element X implemented? | Ch. 4 / Ch. 5 |
| What's the type of X? | Ch. 6 |
| How do mentions / mrkdwn get parsed? | Ch. 8 §8.2 |
| How does styling / dark mode work? | Ch. 9 |
| How do I ship a change? | Ch. 10 / Ch. 11 |
| What rule governs X? | Ch. 14 (conventions) |
Update the knowledge base in the same PR as the code change, never in a follow-up. Treat these as mandatory triggers:
- A block or element is added, renamed, removed, or has its props changed → update Ch. 4 / Ch. 5 + Ch. 6.
- A public export is added or removed from
src/index.ts→ update Ch. 3. - Build tooling changes (
tsup.config.ts,tsconfig.json,tailwind.config.js,postcss.config.js) → update Ch. 9 / Ch. 10. - Peer deps, runtime deps, Node/React target shifts → update Ch. 2.
- A file moves, is deleted, or a folder is renamed → update Ch. 13.
- The release or publish process changes → update Ch. 11.
- A new architectural rule emerges (or an existing one changes) → update Ch. 14.
- A new test fixture is added under
test-blocks/→ update Ch. 12.
If the KB and code diverge by even one commit, the KB stops being trustable. Don't let that happen.
- Find the affected chapter via the TOC (do not grep the whole file — use the index).
- Edit only the relevant section. Keep style consistent with §0.5.
- Add a new section if needed: give it a number (
§4.8), add a TOC row, and match the existing anchor format (lowercase, hyphen-separated, strip punctuation). - If you break an existing anchor (renamed a heading), search for
§X.Ycross-references and update them. - Log the change in Ch. 15 — Change log of this doc with: date, author, one-line summary.
- Review the diff: TOC row, body section, change log row — all three should appear.
- Tables for enumerations ("list of X with properties Y and Z").
- Prose for concepts and rationale.
- Code blocks for type signatures, short examples, prop shapes — never for paragraphs.
- Backticks for file paths, identifiers, tool names, package names.
- Don't duplicate source. Summarize intent and link to the file path; the code is the truth.
- Self-contained sections. Assume the reader lands mid-book; don't say "as we saw above" without a
§reference. - Every new heading gets a TOC row.
Whoever makes the code change owns the KB update for that change. When Claude/Cowork is driving, I update the KB in the same turn as the code edit and log it in Ch. 15. If you ever spot KB drift, flag it and I'll reconcile.
| Field | Value |
|---|---|
| Name | slack-blocks-to-jsx |
| Current version | 1.0.3 (see package.json) |
| License | MIT |
| Author | Manish Panwar (codeemash@gmail.com) |
| Package manager | pnpm |
| Node target | ES2016 via tsup |
| Keywords | slack, slack-blocks, slack-blocks-to-jsx, slack-blocks-to-react, slack-blocks-to-react-comp |
Renders a Slack Block Kit blocks[] payload as React/JSX with pixel-close styling to the real Slack UI. Used to preview Slack messages, modals, and interactive surfaces inside any React app. Ships CJS, ESM, type declarations, and a single compiled CSS file.
- Repository:
https://github.com/themashcodee/slack-blocks-to-jsx.git - npm:
https://www.npmjs.com/package/slack-blocks-to-jsx
npm install slack-blocks-to-jsx
# or
pnpm add slack-blocks-to-jsx.npmrc enables auto-install-peers=true. Since v1.0.1, react-markdown and remark-gfm are externalized, so downstream bundlers (Next.js etc.) must be able to resolve them.
| Package | Version | Purpose |
|---|---|---|
node-emoji |
^2.1.3 |
Convert :emoji_name: → unicode |
react-markdown |
^9.1.0 |
Renders the markdown block (standard GFM) |
remark-gfm |
^4.0.1 |
GFM plugin for the above |
Users must import the compiled stylesheet once in their app entry:
import "slack-blocks-to-jsx/dist/style.css";All classes are prefixed under #slack_blocks_to_jsx (see §9.1) so they do not leak.
export * from "./message"; // Message, MessageProps
export * from "./types"; // Block, Element, TextObject, etc.Nothing else is intentionally public.
File: src/message.tsx.
type MessageProps = {
blocks: Block[];
logo: string;
name: string;
time?: Date; // defaults to new Date()
className?: string;
style?: React.CSSProperties;
theme?: "light" | "dark";
showBlockKitDebug?: boolean; // shows a "Open in Block Kit Builder" link
unstyled?: boolean; // default false; skip shipping classes
withoutWrapper?: boolean; // skip the outer container + header
data?: {
users?: { id: string; name: string }[];
channels?: { id: string; name: string }[];
user_groups?: { id: string; name: string }[];
};
hooks?: GlobalStore["hooks"]; // see §7.2
};Rendered DOM shape:
<div id="slack_blocks_to_jsx" data-theme="light|dark">
<header name="{name}" time="{time}" />
<div class="slack_blocks_to_jsx--blocks">
{blocks.map(b => <BlockWrapper>{getBlockComponent(b)}</BlockWrapper>)}
</div>
</div>File: src/header.tsx. Renders app name (bold) + APP badge + HH:mm timestamp.
File: src/block_wrapper.tsx. Thin wrapper that sets dark-mode text color and break-words.
File: src/components/index.tsx.
getBlockComponent(block: Block): ReactElement | null
getElementComponent(element: Element): ReactElement | nullSwitch on block.type / element.type. Returns null for unsupported types, so unknown Slack block additions won't crash — they just render nothing.
All block components live in src/components/blocks/. 17 block types are supported.
| Block | File | Key props | Notes |
|---|---|---|---|
section |
section.tsx |
text, fields, accessory, expand, block_id |
Workhorse block; accessory can stack vertically (see utils/is_accessory_stacked.ts) |
divider |
divider.tsx |
block_id |
Horizontal rule |
header |
header.tsx |
text (plain_text, ≤150), block_id, level? (1-4) |
Big bold title. level 1-4 maps to H1-H4 (size-distinguished); omit for legacy single-size rendering. |
| Block | File | Key props |
|---|---|---|
image |
image.tsx |
image_url or slack_file, alt_text, title, block_id; collapsible title |
video |
video.tsx |
video_url, thumbnail_url, title, alt_text, title_url, block_id; collapsible; YouTube/Vimeo friendly |
file |
file.tsx |
external_id, source="remote", block_id |
| Block | File | Key props |
|---|---|---|
actions |
actions.tsx |
elements[] (≤25) — routes to getElementComponent for every supported element type |
input |
input.tsx |
label, element (any input element), optional, hint, dispatch_action_config |
context_actions |
context_actions.tsx |
elements[] (feedback/icon buttons for AI-response affordances) |
| Block | File | Notes |
|---|---|---|
context |
context.tsx |
Up to 10 mixed text + image elements |
table |
table.tsx |
cells: Cell[][], optional size; cell alignment; bordered |
markdown |
markdown_block.tsx |
Renders standard GFM via react-markdown + remark-gfm; tables, task lists, code blocks, syntax highlight. Emoji shortcodes are interpolated by the remarkSlackEmoji plugin, reusing the same <SlackEmoji> component (and hooks.emoji) as the mrkdwn parser |
| Block | File | Key props |
|---|---|---|
plan |
plan.tsx |
title: string, tasks: TaskCardBlock[] — sequential progress UI |
task_card |
task_card.tsx |
task_id, title, status: pending | in_progress | complete | error, details?, output?, sources? |
| Block | File | Key props | Notes |
|---|---|---|---|
alert |
alert.tsx |
text: TextObject, level?: "default" | "info" | "warning" | "error" | "success", block_id? |
Single-line status banner. Each level gets its own icon and tinted border/background that adapts to light/dark theme. role="status" for screen readers. Unknown level falls back to default. |
card |
card.tsx |
title?, subtitle?, body?, hero_image?: CardImage, icon?: CardImage, actions?: ButtonElement[] (≤5), block_id?; supports inCarousel prop when nested |
Rich container. Actions reuse the existing ButtonElement renderer (so primary/danger styles and confirm dialogs work for free). Hero image renders in a 16:9 frame above the title; icon renders inline next to the title. |
carousel |
carousel.tsx |
elements: CardBlock[] (1–10), block_id? |
Horizontally-scrollable gallery using CSS scroll-snap. Slides cap at 10; extra elements are dropped silently. Each slide passes inCarousel to the Card component for a fixed-width variant. |
Folder: src/components/blocks/rich_text/. The most involved block by far.
Dispatcher: rich_text.tsx switches on each entry of elements: RichTextBlockElement[] and delegates to:
rich_text_section.tsx— text elements (text, link, emoji, user, usergroup, channel, broadcast, color, preformatted)rich_text_list_wrapper.tsx— ordered/unordered lists, nestingrich_text_section_color.tsx— colored background textrich_text_section_emoji.tsx— emoji + skin tonerich_text_section_link.tsxrich_text_section_user.tsx,..._usergroup.tsx,..._channel.tsx,..._broadcast.tsx
Each text sub-element supports a style: { code?, bold?, italic?, strike? } map. Broadcasts are @channel, @here, @everyone.
All element components live in src/components/elements/. The dispatcher is getElementComponent.
| Element | File |
|---|---|
button |
button_element.tsx — text, action_id, url, value, style: primary|danger, confirm, accessibility_label |
workflow_button |
workflow_button_element.tsx — triggers a Slack workflow; supports primary/danger styles |
| Element | File | Notes |
|---|---|---|
static_select |
static_select_element.tsx |
options[] or option_groups[], initial_option, placeholder, confirm |
external_select |
external_select_element.tsx |
min_query_length |
users_select |
users_select_element.tsx |
initial_user |
conversations_select |
conversations_select_element.tsx |
filter, default_to_current_conversation |
channels_select |
channels_select_element.tsx |
response_url_enabled |
| Element | File |
|---|---|
multi_static_select |
multi_static_select_element.tsx |
multi_external_select |
multi_external_select_element.tsx |
multi_users_select |
multi_users_select_element.tsx |
multi_conversations_select |
multi_conversations_select_element.tsx |
multi_channels_select |
multi_channels_select_element.tsx |
All support max_selected_items and confirm.
| Element | File |
|---|---|
radio_buttons |
radio_buttons_element.tsx — options[], initial_option, confirm |
checkboxes |
checkboxes_element.tsx — options[], initial_options[], confirm |
| Element | File |
|---|---|
plain_text_input |
plain_text_input.tsx — multiline, min_length, max_length, dispatch_action_config |
email_text_input |
email_input_element.tsx |
url_text_input |
url_input_element.tsx |
number_input |
number_input_element.tsx — is_decimal_allowed, min_value, max_value |
| Element | File | Value format |
|---|---|---|
datepicker |
date_picker_element.tsx |
YYYY-MM-DD |
timepicker |
time_picker_element.tsx |
HH:mm, optional timezone |
datetimepicker |
datetime_picker_element.tsx |
Unix timestamp |
These are the elements that trigger vertical stacking when used as section accessories (see utils/is_accessory_stacked.ts).
| Element | File |
|---|---|
file_input |
file_input_element.tsx — filetypes[], max_files |
rich_text_input |
rich_text_input_element.tsx — initial_value, placeholder |
| Element | File | Purpose |
|---|---|---|
image |
image_element.tsx |
Display-only image; image_url or slack_file |
overflow |
overflow_menu_element.tsx |
3-dot menu, up to 5 options |
feedback_buttons |
feedback_buttons_element.tsx |
thumbs up / down |
icon_button |
icon_button_element.tsx |
small icon action (e.g. trash) |
url |
url_source_element.tsx |
Clickable URL reference for task cards |
Types live in src/types/ and are re-exported from index.ts.
Defines the Block union: ActionsBlock | AlertBlock | CardBlock | CarouselBlock | ContextBlock | ContextActionsBlock | DividerBlock | FileBlock | HeaderBlock | ImageBlock | InputBlock | MarkdownBlock | PlanBlock | SectionBlock | TableBlock | TaskCardBlock | VideoBlock | RichTextBlock. Each interface documents props with JSDoc pointing to Slack's official API docs.
Added in v1.1.0 to cover Slack's 2026-04-16 block launch:
AlertLevel = "default" | "info" | "warning" | "error" | "success"AlertBlock = { type: "alert"; text: TextObject; level?: AlertLevel; block_id? }CardImage = { image_url?: string; slack_file?: SlackFileObject; alt_text: string }(shared helper for hero + icon)CardBlock = { type: "card"; title?; subtitle?; body?; hero_image?: CardImage; icon?: CardImage; actions?: ButtonElement[]; block_id? }CarouselBlock = { type: "carousel"; elements: CardBlock[]; block_id? }
Defines the Element union for all 28 interactive/display elements. Every element carries optional action_id and confirm (except display-only ones).
Composition objects:
TextObject<T = "plain_text" | "mrkdwn">—{ type, text, emoji?, verbatim? }OptionObject—{ text, value, description?, url? }OptionGroupObject—{ label, options[] }ConfirmDialogObject—{ title, text, confirm, deny, style? }DispatchActionConfigObject—{ trigger_actions_on?: ("on_enter_pressed" | "on_character_entered")[] }FilterObject— for conversation/channel selectsSlackFileObject—{ url?: string; id?: string }TriggerObject,WorkflowObject— workflow_button payloadsStyle—"primary" | "danger" | "confirm"
RichTextBlockElement union: text, link, emoji, user, usergroup, channel, broadcast, color, preformatted, rich_text_list, rich_text_quote, rich_text_section. Shared Style = { code?, bold?, italic?, strike? }.
State is kept in a React context defined in src/store/useGlobalContext.tsx.
Passed through to GlobalProvider. Used by the mention tokenizers so <@U123> can render as @manish instead of the raw id.
data?: {
users?: { id: string; name: string }[]
channels?: { id: string; name: string }[]
user_groups?: { id: string; name: string }[]
}Full shape:
type Hooks = {
user?: (d: { id: string; name: string }) => ReactNode;
channel?: (d: { id: string; name: string }) => ReactNode;
usergroup?: (d: { id: string; name: string }) => ReactNode;
atChannel?: (style?) => ReactNode;
atEveryone?: (style?) => ReactNode;
atHere?: (style?) => ReactNode;
emoji?: (d, parse: () => ReactNode) => ReactNode;
date?: (d) => ReactNode;
link?: (input) => ReactNode; // the whole <a> replacement
};Useful for wiring mentions to internal routing or using Next.js <Link> for URLs.
Internal hook used by every component that needs access to data or hooks. Don't re-export; keep private.
Message → GlobalProvider → for each block:
BlockWrapper → getBlockComponent(block) → one of the blocks/*.tsx files
Blocks that contain elements call getElementComponent(element)
Text objects go through text_object.tsx (§8.4)
Folder: src/utils/markdown_parser/. Built on @yozora/parser with six custom Slack-specific tokenizers so Slack mrkdwn renders correctly.
| Syntax | Conversion |
|---|---|
*text* |
→ **text** (bold) |
_text_ |
italic |
~text~ |
→ ~~text~~ (strikethrough) |
`code` |
inline code |
<url|label> |
→ [label](url) |
Custom tokenizers (each has its own folder with match.ts, parse.ts, tokenizer.ts, types.ts):
slack_user_mention—<@U123>/<@U123|name>slack_channel_mention—<#C123>/<#C123|name>slack_user_group_mention—<!subteam^G123>/<!subteam^G123|@name>slack_broadcast—<!channel>,<!here>,<!everyone>slack_date—<!date^ts^format|fallback>slack_emoji—:emoji_name:including skin-tone variants
Output is ReactNode via the elements/ and sub_elements/ folders (Blockquote, Code, Paragraph + Delete, Emphasis, InlineCode, Link, Strong, Text, HTML, SlackUserMention, etc.).
Folder: src/utils/emojis/. parser.ts + list.ts. Handles :emoji: resolution and skin tone modifiers (1–6). Uses node-emoji at runtime.
Folder: src/components/composition_objects/.
text_object.tsx— rendersTextObject. Formrkdwnruns through the markdown parser above and applieshooks.linkto any<a>.confirm_dialog.tsx— modal overlay forConfirmDialogObject; click-outside cancels; supportsprimary(green) /danger(red) styles; full dark mode.
tailwind.config.js sets important: "#slack_blocks_to_jsx" so every utility is compiled as #slack_blocks_to_jsx .class { … !important }. This guarantees styles win against arbitrary host-app CSS without polluting it.
Theme extensions include Slack-accurate grays, blacks, blues, greens, reds, broadcast colors — all with dark-mode variants. Font sizes: small: 13px, base: 15px, header: 18px. Tailwind scans ./src/**/*.{js,ts,jsx,tsx,mdx}.
darkMode: ["selector", '[data-theme="dark"]']. Toggled at runtime via the theme prop on Message which writes data-theme="dark" on the outer wrapper.
postcss.config.js pipeline (in order):
postcss-nestingtailwindcssautoprefixercssnano
Input: src/style.css. Output: dist/style.css (~40 KB).
tsup.config.ts compiles src/index.ts to CJS + ESM, generates .d.ts, minifies, and marks react-markdown, remark-gfm, node-emoji as external.
target: ES2016,module: CommonJS,jsx: react-jsxstrict: true,noUncheckedIndexedAccess: true,skipLibCheck: truenoEmit: true(tsup emits)
| Script | What it does |
|---|---|
build |
tsup build + build:css |
build:css |
PostCSS src/style.css → dist/style.css |
dev |
tsup watch |
dev:css |
PostCSS watch |
lint |
tsc --noEmit |
test |
tsc --noEmit (typecheck; no unit tests) |
release |
node scripts/release.mjs (see Ch. 11) |
release:dry |
Preview a release; changes nothing |
release:beta |
Publish next beta prerelease (tag: beta) |
release:alpha |
Publish next alpha prerelease (tag: alpha) |
| File | Size | Purpose |
|---|---|---|
index.js |
~556 KB | CJS bundle (minified) |
index.mjs |
~552 KB | ESM bundle (minified) |
index.d.ts / index.d.mts |
~82 KB | TS declarations |
style.css |
~40 KB | Compiled Tailwind bundle |
.npmignore excludes: .github, scripts, RELEASING.md, node_modules, src, .env, .prettierrc, CHANGELOG.md, pnpm-lock.yaml, tailwind.config.js, tsconfig.json, .DS_Store, postcss.config.js.
.npmrc: auto-install-peers=true.
package.json exports: main (CJS), module (ESM), types.
Releasing is a single local command: pnpm release (see RELEASING.md). It is driven by
scripts/release.mjs — a dependency-free Node script that, in order:
- Pre-flight checks — clean working tree, on
main, not behindorigin/main, and authenticated to both npm (npm whoami) and GitHub (gh auth status). - Lint / typecheck (
pnpm run lint) and build (pnpm run build). - Bumps the version in
package.json(interactive patch/minor/major/custom, or passed as an arg). - Commits (
chore: release vX.Y.Z) and tags (vX.Y.Z). - Publishes to npm (
pnpm publish --no-git-checks --tag <dist-tag>). - Pushes the commit + tag, then creates a GitHub release via
gh release create --generate-notes.
pnpm release:dry previews the whole thing (runs checks + build, mutates nothing).
Prereleases (beta / alpha): pnpm release:beta / pnpm release:alpha cut the next prerelease
(1.0.4 → 1.0.5-beta.0 → -beta.1 …). Each publishes under its own npm dist-tag (beta, alpha,
rc…) so it never becomes latest, and is flagged as a pre-release on GitHub — testers opt in with
npm install slack-blocks-to-jsx@beta. Bump types prepatch/preminor/premajor/prerelease plus
--preid=<id> give finer control; a normal pnpm release patch on a prerelease graduates it to stable
(1.0.5-beta.2 → 1.0.5). The version maths mirror semver's inc. See RELEASING.md.
The old Changesets flow (pnpm changeset + the publish.yml "Version Packages" bot) has been removed
in favour of this. CI (.github/workflows/main.yml) still runs lint + build on every push/PR, but no
longer publishes — releasing is now a deliberate local action. There is no CHANGELOG.md; release notes
are auto-generated from merged PRs/commits on each GitHub release.
PR preview releases: .github/workflows/preview.yml publishes every PR (and main) to
pkg.pr.new so reviewers can npm install
a branch (https://pkg.pr.new/slack-blocks-to-jsx@<commit-or-PR>) without it ever hitting the
real npm package. It needs no npm token, works for fork PRs, and re-publishes + updates the PR
comment on every new commit. Requires the pkg.pr.new GitHub App installed on the repo. These
previews are distinct from intentional pnpm release:beta prereleases (which do go to npm).
RELEASE_NOTES.md tells the story of v1.0.1 — the "full Block Kit parity" release — after 80+ iterative releases. Major breaking changes from that version:
react-markdown+remark-gfmexternalized (host apps must transpile them in Next.js).- Actions block now routes all element types; previously silently dropped unsupported ones.
- Input block accepts all input element types; previously only
plain_text_inputandcheckboxes.
New in 1.0.1: Context Actions, Markdown, Plan, Task Card blocks; 22 new interactive elements; confirm dialogs everywhere; dark mode via data-theme; GFM markdown; Slack-hosted files for images; expand on section.
Current version at time of writing: 1.0.3.
Folder: test-blocks/. Used as manual/visual test inputs. Each file is a complete blocks[] payload.
| File | Covers |
|---|---|
00-all-blocks-part1.json / part2.json |
Kitchen-sink of every block type |
01-rich-text-nested.json |
Nested rich text formatting |
02-actions-confirm-dialogs.json |
Actions + confirm modals |
03-all-input-types.json |
All input elements |
04-option-groups-multi-selects.json |
Option groups + multi-selects |
05-markdown-block.json |
GFM markdown (tables, code, task lists) |
06-plan-task-cards.json |
Plan + task card statuses |
07-context-actions-feedback.json |
Feedback buttons |
08-table-image-video.json |
Table + image + video |
09-pr-review-workflow.json |
Workflow buttons |
10-kitchen-sink.json |
Large combined payload |
11-alert-card-carousel.json |
All 5 alert levels, card (hero + icon + actions), standalone card, 3-card pricing carousel |
slack-safe-part1.json |
Compatibility fixture |
slack blocks to jsx library/
├── .github/
├── scripts/
│ └── release.mjs # One-command release (see Ch. 11)
├── dist/
│ ├── index.js index.mjs index.d.ts index.d.mts style.css
├── src/
│ ├── index.ts # Public exports
│ ├── message.tsx # <Message> (entry)
│ ├── header.tsx # App header subcomponent
│ ├── block_wrapper.tsx # Per-block wrapper
│ ├── style.css # Tailwind source
│ ├── components/
│ │ ├── index.tsx # getBlockComponent / getElementComponent
│ │ ├── blocks/
│ │ │ ├── section.tsx divider.tsx image.tsx context.tsx
│ │ │ ├── header.tsx actions.tsx input.tsx video.tsx
│ │ │ ├── file.tsx table.tsx markdown_block.tsx
│ │ │ ├── plan.tsx task_card.tsx context_actions.tsx
│ │ │ ├── alert.tsx card.tsx carousel.tsx (new in v1.1.0)
│ │ │ └── rich_text/
│ │ │ ├── rich_text.tsx
│ │ │ ├── rich_text_section.tsx
│ │ │ ├── rich_text_list_wrapper.tsx
│ │ │ ├── rich_text_section_color.tsx
│ │ │ ├── rich_text_section_emoji.tsx
│ │ │ ├── rich_text_section_link.tsx
│ │ │ ├── rich_text_section_user.tsx
│ │ │ ├── rich_text_section_usergroup.tsx
│ │ │ ├── rich_text_section_channel.tsx
│ │ │ └── rich_text_section_broadcast.tsx
│ │ ├── elements/ # 28 *_element.tsx files (see Ch. 5)
│ │ └── composition_objects/
│ │ ├── text_object.tsx
│ │ └── confirm_dialog.tsx
│ ├── store/
│ │ └── useGlobalContext.tsx # GlobalProvider + useGlobalData
│ ├── types/
│ │ ├── layout.ts elements.ts objects.ts rich_text_element.ts
│ └── utils/
│ ├── date.ts
│ ├── is_accessory_stacked.ts
│ ├── merge_classes.ts
│ ├── numbers.ts
│ ├── remark_slack_emoji.ts # emoji rule for the markdown block (react-markdown/mdast)
│ ├── sanitize_for_slack.ts
│ ├── emojis/{parser.ts, list.ts}
│ └── markdown_parser/
│ ├── parser.tsx types.ts
│ ├── elements/{blockquote.tsx, code.tsx, paragraph.tsx}
│ ├── sub_elements/
│ │ ├── delete.tsx emphasis.tsx html.tsx inline_code.tsx
│ │ ├── link.tsx slack_broadcast.tsx slack_channel_mention.tsx
│ │ ├── slack_date.tsx slack_emoji.tsx
│ │ ├── slack_user_group_mention.tsx slack_user_mention.tsx
│ │ ├── strong.tsx text.tsx
│ └── tokenizers/
│ ├── slack_user_mention/
│ ├── slack_channel_mention/
│ ├── slack_user_group_mention/
│ ├── slack_broadcast/
│ ├── slack_date/
│ └── slack_emoji/
├── test-blocks/ # JSON fixtures (see Ch. 12, gitignored)
├── playground/ # Contributor preview app (see Ch. 15, not in npm)
│ ├── package.json vite.config.ts tsconfig.json tsconfig.node.json
│ ├── tailwind.config.cjs postcss.config.js index.html README.md
│ └── src/
│ ├── main.tsx App.tsx fixtures.ts index.css
├── package.json tsconfig.json tsup.config.ts
├── tailwind.config.js postcss.config.js .prettierrc
├── readme.md RELEASING.md KNOWLEDGE_BASE.md
├── .npmignore .npmrc .env .gitignore
└── pnpm-lock.yaml
Rules worth knowing before making changes:
- File naming: snake_case for all files in
src/(section.tsx,button_element.tsx). - Dispatcher pattern: never
switchonblock.typeinside a block component. Always letcomponents/index.tsxroute. Returningnullfor unsupported types is intentional — it means forward-compat without crashes. - Tailwind scope: every visible component must render inside the
#slack_blocks_to_jsxwrapper — otherwiseimportantselectors don't match. - Dark mode: use Tailwind
dark:variants. Don't readthemeprop directly in components; rely on thedata-themeattribute and CSS. - Mentions: never render raw
<@U123>in components. Use the markdown parser so tokenizers andhooksfire. - Hooks over forking: anything that needs host-app integration (routing, avatars, emoji catalog) should be exposed via the
hooksprop, not hardcoded. - Externalization:
react-markdown,remark-gfm,node-emojimust stay external in tsup config — bundling them again will break Next.js ESM interop. - Accessibility: preserve the
accessibility_labelprop on buttons; use semantic tags (button, a, input) — this mirrors Slack's own a11y posture. - Type-first: every block/element addition requires a matching interface in
src/types/before the component lands. - Releasing: versioning + publishing is a single local command,
pnpm release(see Ch. 11 /RELEASING.md). No changeset files are needed.
A tiny Vite + React app that lives in playground/ at the repo root. It imports the library from source (../src/index.ts) via a Vite alias, so any edit under src/ — components, types, styles — hot-reloads instantly in the preview pane without running tsup or postcss by hand.
The playground is for contributors. It is committed to git (so anyone who clones the repo can run it) but it is excluded from the published npm tarball (see §15.5).
playground/
├── package.json # isolated deps (react 18, vite 5, tailwind 3)
├── vite.config.ts # aliases "slack-blocks-to-jsx" → ../src/index.ts
├── tsconfig.json # paths → ../src/index.ts (editor go-to-def works)
├── tsconfig.node.json # vite.config's own tsconfig
├── tailwind.config.cjs # re-exports ../tailwind.config.js, extends content
├── postcss.config.js # mirrors library pipeline minus cssnano
├── index.html
├── .gitignore # node_modules, dist, .vite
├── README.md
└── src/
├── main.tsx # React 18 createRoot entry
├── App.tsx # sidebar + JSON editor + preview + theme toggle
├── index.css # @imports ../../src/style.css + plain-CSS chrome
└── fixtures.ts # inline sample block payloads
The Vite alias "slack-blocks-to-jsx" → ../src/index.ts means the playground never touches dist/. Editing src/components/blocks/alert.tsx triggers Vite's React Fast Refresh; editing src/style.css or tailwind.config.js triggers a CSS reload because src/index.css uses @import "../../src/style.css" and the playground's PostCSS pipeline runs the same Tailwind + nesting + autoprefixer chain the library ships with.
Fixtures are kept inline in playground/src/fixtures.ts instead of reading from ../test-blocks/ because test-blocks/ is gitignored and therefore absent from fresh clones.
From the library root:
pnpm playground:install # one-time, installs the playground's deps
pnpm playground # starts Vite dev server on http://localhost:5173There is also pnpm playground:build for a production build of the playground (useful if you ever want to deploy it as a demo page).
Three guards keep the playground out of the published package:
.npmignorelistsplayground/, sonpm publishskips the folder entirely.- The playground has its own
package.jsonwith its ownnode_modules, so its deps never leak into the library's dep graph. - The library's build (
tsup) only readssrc/; it never resolves anything underplayground/.
Verify with npm pack --dry-run after any change — the tarball file list should never contain a playground/ entry.
Open playground/src/fixtures.ts and append a new entry to the FIXTURES array:
{
id: "my-new-block",
label: "My new block",
blocks: [
{ type: "my_new_block", /* … */ },
],
}The sidebar picks it up automatically on next HMR. Keep fixtures small (≤ 10 blocks) — the goal is "open the playground and immediately see how this block renders", not full regression coverage.
| Date | Author | Note |
|---|---|---|
| 2026-04-17 | Claude + Mash | Initial knowledge base created from full source walkthrough at v1.0.3. |
| 2026-04-17 | Claude + Mash | Added Ch. 0 — How to use this knowledge base (usage, reading strategy, update triggers, update procedure, style rules, ownership). |
| 2026-04-17 | Claude + Mash | v1.1.0 release prep: added three new blocks (alert, card, carousel) matching Slack's 2026-04-16 Block Kit launch. Ch. 4 block count 14 → 17 (new §4.6 "Status & rich-container blocks"). Ch. 6.1 type list updated with AlertBlock, CardBlock, CarouselBlock, CardImage, AlertLevel. Ch. 12 gained 11-alert-card-carousel.json fixture. Ch. 13 file tree updated. Changeset: .changeset/add-alert-card-carousel-blocks.md. |
| 2026-04-17 | Claude + Mash | Added Ch. 15 — Playground. New playground/ folder (Vite + React 18) that source-aliases slack-blocks-to-jsx to ../src/index.ts for instant HMR. Committed to git, excluded from npm via .npmignore. Ch. 15 renumbered from change log → playground; change log moved to Ch. 16. Root package.json gained playground, playground:install, playground:build scripts. Ch. 13 file tree extended. |
| 2026-06-06 | Claude + Mash | Replaced the Changesets release flow with a single local command pnpm release (scripts/release.mjs): checks → lint → build → bump → commit/tag → npm publish → push → GitHub release. Removed .changeset/, @changesets/cli, and .github/workflows/publish.yml; added test/release/release:dry scripts and RELEASING.md. Rewrote Ch. 11, updated §10.3/§10.5 scripts + hygiene, Ch. 13 file tree (scripts/), and Ch. 14 rule 10. Added first-class beta/alpha prereleases: release:beta/release:alpha scripts, prepatch/preminor/premajor/prerelease bump types + --preid, auto dist-tag per channel, prerelease graduation, and a "Trying a prerelease" section in readme.md. Added .github/workflows/preview.yml (pkg.pr.new) for automatic per-PR/per-commit preview installs (no npm token, fork-safe). |
{ "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19", }