Slack Markdown Formatting
Every Slack formatting syntax with copy-ready examples — and which of Slack's three formatting surfaces you are actually typing into, because they do not share a syntax.
Short answer
Slack's own format is mrkdwn, which is not Markdown: *bold*, _italic_, ~strike~, <url|text>, and no lists, headings or tables at all. But that is only one of three surfaces. The message composer accepts standard shortcuts like **bold** and converts them as you type, and since 2025 an app can send a Block Kit markdown block that takes real standard Markdown — headings, lists, tables, task lists and syntax-highlighted code included.
Where Markdown Works in Slack
Almost every “why is my Slack formatting broken” question is really this question. Slack has three places text gets formatted, and each one parses a different syntax.
1. The composer
Typing in Slack
WYSIWYG by default, with a formatting toolbar. It is the forgiving one: it accepts **bold** and [text](url) and converts them for you.
2. mrkdwn
The API message format
The text field and every mrkdwn text object in Block Kit. The strict one: single asterisks, angle-bracket links, no lists or headings.
3. markdown block
Standard Markdown, for apps
A Block Kit block that takes ordinary Markdown and converts it itself. The only surface with tables, task lists and highlighted code.
| Feature | Composer | mrkdwn (API) | markdown block |
|---|---|---|---|
| Bold | **bold** or ⌘B | *bold* | **bold** |
| Italic | *italic* or ⌘I | _italic_ | *italic* |
| Strikethrough | ~~strike~~ or ⌘⇧X | ~strike~ | ~~strike~~ |
| Underline | ⌘U — no syntax | Not supported | Not supported |
| Link | [text](url) or ⌘⇧U | <url|text> | [text](url) |
| Inline code | `code` or ⌘⇧C | `code` | `code` |
| Code block | ``` or ⌘⌥⇧C | ``` — no highlighting | ```python — highlighted |
| Blockquote | > quote or ⌘⇧9 | > quote | > quote |
| Bulleted list | * then space, or ⌘⇧8 | Not supported | - item |
| Numbered list | 1. then space, or ⌘⇧7 | Not supported | 1. item |
| Heading | Not supported | Not supported | # Heading |
| Table | Not supported | Not supported | | Col | Col | |
| Task list | Not supported | Not supported | - [ ] task |
| Divider | Not supported | Not supported | --- |
| Image | Upload or paste | Not supported |  → link text |
Slack Text Formatting (mrkdwn)
This is the syntax that makes Slack different from every other Markdown. Because most renderers get it wrong, the right-hand panel below shows what Slack actually displays rather than a Markdown preview.
Slack uses one asterisk for bold. Two asterisks stay literal in mrkdwn.
*bold text*Underscores around the text. Asterisks mean bold in mrkdwn, not italic.
_italic text_One tilde each side. The GitHub-style ~~double tilde~~ is not mrkdwn.
~strikethrough~Nest underscores inside asterisks. mrkdwn has no triple-marker shortcut.
*_bold italic_*_italic_ means italic in mrkdwn and in standard Markdown. The asterisk is the character that flips meaning between the two, so text written with underscores survives being moved between surfaces.Standard Markdown in Slack: the markdown block
Slack introduced a Block Kit markdown block on 3 February 2025 and widened it on 6 March 2026 with tables, task lists, dividers, syntax-highlighted code blocks and variable-sized headers. It takes standard Markdown and does the conversion to Slack's internal format itself — so inside one, **bold** is correct and *bold* means italic, exactly backwards from mrkdwn.
Slack's own stated reason for it is AI: when an LLM hands your app Markdown, a markdown block lets Slack translate it instead of you writing a converter. If you are formatting model output for Slack, this is the block to reach for — see our Markdown for AI guide for the wider pattern.
{
"blocks": [
{
"type": "markdown",
"text": "## Deploy finished\n\n- [x] tests green\n- [ ] changelog\n\n| env | status |\n| --- | ------ |\n| prod | green |"
}
]
}**bold**, *italic*, ~~strikethrough~~ and `inline code`bold, italic, strikethrough and inline code
# Header 1
## Header 2
### Header 3Header 1
Header 2
Header 3
- first item
- second item
- nested item
1. first step
2. second step- first item
- second item
- nested item
- first step
- second step
- [x] shipped the release notes
- [ ] update the changelog- shipped the release notes
- update the changelog
| Environment | Status |
| ----------- | ------ |
| production | green |
| staging | red || Environment | Status |
|---|---|
| production | green |
| staging | red |
Above the line
---
Below the lineAbove the line
Below the line
Also available: markdown_text
chat.postMessage takes a markdown_text argument that accepts the same Markdown without building a block array. Slack's docs are explicit that it must not be combined with blocks or text, and caps it at 12,000 characters.
Two limits worth knowing
Every heading level renders at the same size, so # and ###### look identical. And  does not embed an image — Slack turns it into hyperlink text. Use an image block for a real image.
Links
mrkdwn masks links with angle brackets and a pipe rather than [text](url). Both forms exist on Slack — which one is correct depends on the surface, and getting it wrong is the difference between a link and a line of visible punctuation.
Angle brackets, then a pipe, then the display text. This is what the API expects.
<https://slack.com|Slack>A bare URL also links itself; the brackets just make the boundary explicit.
<https://slack.com>Same shape, with a mailto: scheme instead of https:.
<mailto:me@example.com|Email me>Works when you type it in Slack, and inside a markdown block. In a plain mrkdwn field it stays literal.
[Slack](https://slack.com)Mentions and Special Tokens
Mentions are IDs wrapped in angle brackets, not display names. The composer autocompletes @name into the right ID for you; an API message has to arrive with the ID already in it, or it is just text and nobody gets notified. Slack recommends the explicit form precisely because names change.
The member ID, never the display name — display names change, IDs do not.
<@U012AB3CD>The channel ID. Slack fills in the current channel name when it renders.
<#C123ABC456>The user-group ID after a caret. Renders as the group's handle.
<!subteam^S0NHM1FH4>Only pings people currently active in the channel.
<!here>Pings every member. <!everyone> exists too, and only works in #general.
<!channel>Slack renders the token string locally and falls back to the text after the pipe.
<!date^1392734382^{date_short} at {time}|Feb 18, 2014 at 6:39 AM>Lists
Lists exist on two of Slack's three surfaces. In the composer, type * or 1. followed by a space and Slack builds a rich-text list as you go. In a markdown block, ordinary - item syntax renders properly. Plain mrkdwn has no list syntax at all — Slack's docs suggest mimicking one with hyphens and newlines, which is exactly as good as it sounds.
- first item
- second item
- third item- first item
- second item
- third item
1. first step
2. second step
3. third step- first step
- second step
- third step
Sending a real list from an app
A markdown block is the short route. For full control over indentation and numbering, build a rich_text block with rich_text_list elements.
What does not work
Posting { "text": "- one\n- two" } puts two hyphens on screen and nothing else. mrkdwn does not render bullets, indentation or numbering.
Headings
Headings are the thinnest part of Slack formatting. There is no heading button in the composer toolbar and no keyboard shortcut for one; mrkdwn leaves # as a literal character; and a markdown block does accept # through ###### but renders every level at the same size, so the hierarchy you write is not the hierarchy readers see.
For an app that needs one visually distinct title, the Block Kit header block is the reliable answer — it is a structural block rather than a piece of text formatting.
{
"blocks": [
{
"type": "header",
"text": { "type": "plain_text", "text": "Deploy finished" }
},
{
"type": "section",
"text": { "type": "mrkdwn", "text": "Shipped by <@U012AB3CD> at *09:41*." }
}
]
}Note the mixture: a header block takes plain_text only, so no formatting is parsed inside it, while the section below it is mrkdwn and so uses single asterisks. See Markdown headings for how the syntax behaves everywhere else.
Slack Markdown Tables
“Slack does not support tables” was true for a decade and is now out of date. There are three answers, depending on who is sending the message.
| Service | Owner | Status |
| ------- | ----- | ------ |
| api | Dana | green |
| web | Ravi | amber || Service | Owner | Status |
|---|---|---|
| api | Dana | green |
| web | Ravi | amber |
| Left | Center | Right |
| :--- | :----: | ----: |
| a | b | c || Left | Center | Right |
|---|---|---|
| a | b | c |
From an app, quickly
Put a pipe table in a markdown block. Slack renders it as a real table, alignment colons included.
From an app, with control
The Block Kit table block, added 14 August 2025: up to 100 rows, 20 cells per row and 10,000 characters, with per-column settings.
Typing by hand
Still no table. Paste the rows into a code block so the monospace font holds the columns together, or share a canvas instead.
Need the pipe syntax itself? Our Markdown tables guide covers alignment and escaping, and the table generator builds one you can drop straight into a markdown block.
Quotes and Code Blocks
These are the constructs where all three surfaces agree, with one exception: a language identifier on a code fence is honoured inside a markdown block and silently discarded everywhere else.
> This line is quoted.This line is quoted.
> Line one of the quote.
> Line two continues it.Line one of the quote.
Line two continues it.
`inline code` stays monospacedinline code stays monospaced
```
function hello() {
return "world";
}
```function hello() {
return "world";
}
```python
print("hello")
```print("hello")
The Composer: Shortcuts and the Markup Preference
By default the composer is WYSIWYG — you format with the toolbar or a shortcut and see the result as you type. Slack also ships a preference that turns typing into markup instead: click your profile picture, then Preferences → Advanced, and check “Format messages with markup” under Input options. Formatting is then applied only after the message is sent.
It has two side effects Slack documents and people rarely expect: automatic list formatting is turned off, and pasted messages arrive as plain text. That last one is a feature if you paste code and snippets all day.
| Formatting | Mac | Windows / Linux |
|---|---|---|
| Bold | ⌘ B | Ctrl B |
| Italic | ⌘ I | Ctrl I |
| Underline | ⌘ U | Not documented |
| Strikethrough | ⌘ ⇧ X | Ctrl ⇧ X |
| Inline code | ⌘ ⇧ C | Ctrl ⇧ C |
| Code block | ⌘ ⌥ ⇧ C | Ctrl Alt ⇧ C |
| Blockquote | ⌘ ⇧ 9 | Ctrl ⇧ 9 |
| Bulleted list | ⌘ ⇧ 8 | Ctrl ⇧ 8 |
| Ordered list | ⌘ ⇧ 7 | Ctrl ⇧ 7 |
| Link | ⌘ ⇧ U | Ctrl ⇧ U |
| Apply markup formatting | ⌘ ⇧ F | Ctrl ⇧ F |
| Undo formatting | ⌘ Z | Ctrl Z |
Escaping
Because mrkdwn builds links, mentions and date tokens out of angle brackets, three characters have to be escaped as HTML entities when an app sends mrkdwn. The composer handles this for you; a webhook payload does not.
| Character | Send instead | Why |
|---|---|---|
| & | & | Escaped in mrkdwn so Slack does not read it as the start of an entity. |
| < | < | Otherwise Slack tries to parse it as a link, mention or date token. |
| > | > | Otherwise it can close a mention or start a blockquote. |
Inside a markdown block the rule is the ordinary Markdown one instead: a backslash before the character, so \*not italic\* keeps its asterisks. And to switch parsing off wholesale, set mrkdwn: false on a message, use a plain_text object inside a block, or set verbatim: true to keep formatting but stop Slack auto-linking names.
Common Mistakes
Every one of these comes from applying one surface's rules to another.
Does not work
**bold** in an API `text` fieldUse instead
*bold*Does not work
~~strikethrough~~ in mrkdwnUse instead
~strikethrough~Does not work
[click here](https://example.com) in a section blockUse instead
<https://example.com|click here>Does not work
@alice from a botUse instead
<@U012AB3CD>Does not work
markdown_text alongside blocks or textUse instead
markdown_text on its ownDoes not work
```python in a mrkdwn field, expecting colorsUse instead
```python inside a markdown blockDoes not work
Rebuilding a table as aligned text because "Slack has no tables"Use instead
A markdown block, or a Block Kit table blockSlack Formatting Quick Reference
Every syntax on one screen, with the surface it belongs to.
| Format | Syntax | Notes |
|---|---|---|
| Bold | *text* (mrkdwn) · **text** (composer, markdown block) | The most common source of confusion on Slack. |
| Italic | _text_ (mrkdwn) · *text* (composer, markdown block) | Underscores are the safe choice in both. |
| Strikethrough | ~text~ (mrkdwn) · ~~text~~ (composer, markdown block) | Single tilde is mrkdwn-only. |
| Underline | No syntax — ⌘U in the composer | Stored as a rich-text style, not as markup. |
| Link | <url|text> (mrkdwn) · [text](url) (composer, markdown block) | Both render identically once sent. |
| Autolink | <https://example.com> | Bare URLs also link themselves. |
| User mention | <@U012AB3CD> | Member ID, not display name. |
| Channel mention | <#C123ABC456> | Channel ID. |
| User group | <!subteam^S0NHM1FH4> | Group ID after a caret. |
| Broadcast | <!here> · <!channel> · <!everyone> | @everyone only works in #general. |
| Local timestamp | <!date^unix^{date_short}|fallback> | Rendered in each reader's timezone. |
| Inline code | `code` | Identical on every surface; suppresses other formatting inside. |
| Code block | ```code``` | Highlighting only inside a markdown block. |
| Blockquote | > quote | Repeat > on each line; no >>> shortcut. |
| Bulleted list | - item | Composer and markdown block only — not mrkdwn. |
| Ordered list | 1. item | Composer and markdown block only — not mrkdwn. |
| Heading | # Heading | Markdown block only; all levels render the same size. |
| Table | | a | b | | Markdown block, or the Block Kit table block. |
| Task list | - [ ] task | Markdown block only. |
| Divider | --- | Markdown block, or the Block Kit divider block. |
| Image |  | Becomes hyperlink text — use an image block or upload the file. |
| Emoji | :tada: | Shortcodes expand inline on every surface. |
| Escape | & < > | Required in mrkdwn sent through the API. |
Frequently Asked Questions
Does Slack support Markdown?
Partly, and it depends on where you are typing. Slack's own message format is mrkdwn, which looks like Markdown but is not: a single asterisk means bold, underscores mean italic, and links use <url|text>. The composer accepts standard Markdown shortcuts and converts them as you type. And since February 2025 Slack apps can send a Block Kit markdown block, which does accept real standard Markdown — including headings, lists, tables and task lists.
Why does *text* show as bold in Slack but italic everywhere else?
Slack created mrkdwn before CommonMark stabilised and made different choices. In CommonMark a single asterisk means italic and a double asterisk means bold; mrkdwn uses a single asterisk for bold and an underscore for italic. This is the single biggest reason text pasted from GitHub or Discord looks wrong in a Slack API message.
How do I send a real Markdown message to Slack from a bot?
Use a markdown block, or the markdown_text argument on chat.postMessage. Both take standard Markdown and let Slack do the conversion. markdown_text cannot be combined with blocks or text in the same call, and the combined limit for markdown blocks in one payload is 12,000 characters.
Can Slack display a Markdown table?
Yes, since 2025 — which reverses years of advice to the contrary. A pipe table inside a Block Kit markdown block renders as a real table, and apps can also build one with the dedicated table block (up to 100 rows, 20 cells per row, 10,000 characters). What still has no table support is plain mrkdwn and the message composer, so a table typed by hand into Slack remains a code block with spaces for alignment.
What is the difference between the composer, mrkdwn and a markdown block?
They are three different formatting surfaces. The composer is the input box you type in — WYSIWYG by default, forgiving about syntax. Mrkdwn is Slack's stored message format, used by the text field and by mrkdwn text objects in Block Kit; it is the strict one. The markdown block is a newer Block Kit block that accepts standard Markdown and converts it for you. Which one you are using decides whether *bold* or **bold** is correct.
Does Slack support Markdown headings?
Only inside a markdown block, and all levels render at the same size, so # and ###### look identical. Plain mrkdwn has no heading syntax and leaves the # characters literal, and Slack's message composer has no heading option in its toolbar at all. Apps that want a distinct title should use a Block Kit header block.
Why don't my bullet lists work in Slack messages from the API?
Lists are not part of mrkdwn. Typing - or * in the composer creates a rich-text list, and a markdown block renders - item and 1. item correctly, but a plain mrkdwn text field leaves the hyphens as characters. From an app, send the list in a markdown block or build it with a rich_text block containing rich_text_list elements.
How do I link to a URL with custom text in Slack?
In mrkdwn, use <https://example.com|Click here> — angle brackets around the URL and a pipe before the display text. In the composer and in markdown blocks the standard [Click here](https://example.com) form works instead, and the composer also has ⌘⇧U for turning selected text into a link.
Can I get syntax highlighting in a Slack code block?
In a markdown block, yes — language-specific highlighted code blocks arrived in March 2026, so ```python is honoured. Everywhere else, no: plain mrkdwn and the composer render code blocks in a monospace font with no colouring and silently ignore the language hint.
How do I type Markdown in Slack instead of using the toolbar?
Turn on the markup preference: click your profile picture, choose Preferences, open Advanced, and check "Format messages with markup" under Input options. Your typing then stays plain until the message is sent, at which point Slack applies the formatting. It also turns off automatic list formatting and makes pasted text arrive plain.
Does Slack have underline formatting?
In the composer only, and only through the toolbar or ⌘U on Mac — there is no underline markup to type, and no mrkdwn or markdown-block equivalent. Standard Markdown has the same gap, which is why underlining is usually faked with bold or italic instead.
How do I mention a user from a Slack bot or webhook?
Use <@U012AB3CD> with the member's Slack ID, which you can look up with the users.list method. Channels use <#C123ABC456> and user groups use <!subteam^S0NHM1FH4>. Writing the display name as plain text never produces a notification.
How do I escape special characters in Slack API messages?
Replace the three characters Slack parses as control characters with HTML entities: & for &, < for < and > for >. This applies to mrkdwn sent through the API — the composer escapes for you. Inside a markdown block, use a backslash instead, as in \*not italic\*.
How do I stop Slack formatting a message at all?
There are three levers. Set mrkdwn to false on a top-level message text to disable Slack markup parsing. Use a plain_text text object instead of a mrkdwn one inside a block. Or set verbatim to true on a mrkdwn text object, which keeps the formatting but stops Slack auto-linking channel names and user handles.