If you’re reading this post, you probably came looking for support in rendering a table in Slack’s markdown formatting. Sadly, Slack doesn’t support the traditional markdown table syntax, but we can get creative and come pretty close.
In this post, we’ll demonstrate several workarounds to rendering a table in Slack, including using basic Block Kit layouts as well as rendering an ASCII table inside of a code block.
Video walkthrough
If you prefer to learn with video, watch it here.
Using traditional markdown tables
First, let’s take a quick look at what a traditional markdown table would look like:
| Month | Savings |
| -------- | ------- |
| January | $250 |
| February | $80 |
| March | $420 |
Markdown tables are handy for many reasons, and there are lots of potential use cases for displaying them in Slack apps. Unfortunately, we’ll need to get a little creative to use them in Slack.
For the rest of the examples in this post, let’s assume that we have a JSON object like this one that we’ll use as the data source for our tables:
const data = [
{ Month: "January", Savings: "$250" },
{ Month: "February", Savings: "$80" },
{ Month: "March", Savings: "$420" },
];
Basic table layout with Block Kit
When you break down what a table does, it displays some data using a combination of rows and columns. In most cases, each cell has a border to create some visual separation from the rest of the elements.
Using Slack’s Block Kit, you can approximate a table layout using a heading
block, field
blocks, and a series of dividers
:
While not exactly a table, this gets pretty close and looks great when rendered with Slack, and we did it all with Slack-native formatting options.
The resulting Block Kit JSON looks like this:
{
"blocks": [
{
"text": {
"emoji": true,
"text": "💰 Our Savings",
"type": "plain_text"
},
"type": "header"
},
{
"fields": [
{
"text": "*Month*",
"type": "mrkdwn"
},
{
"text": "*Savings*",
"type": "mrkdwn"
}
],
"type": "section"
},
{
"type": "divider"
},
{
"fields": [
{
"text": "January",
"type": "mrkdwn"
},
{
"text": "$250",
"type": "mrkdwn"
}
],
"type": "section"
},
{
"type": "divider"
},
{
"fields": [
{
"text": "February",
"type": "mrkdwn"
},
{
"text": "$80",
"type": "mrkdwn"
}
],
"type": "section"
},
{
"type": "divider"
},
{
"fields": [
{
"text": "March",
"type": "mrkdwn"
},
{
"text": "$420",
"type": "mrkdwn"
}
],
"type": "section"
},
{
"type": "divider"
}
]
}
Limitations of Block Kit JSON for tables
The main limitation with Block Kit JSON fields
is that you can only display two columns of compact text. If you need to support more columns in your table, you may want to consider the next method using an ASCII table.
Render ASCII table in Slack code block
This approach borders on hacky, but for some applications, it will actually create a pretty good result. Since Slack field
blocks can’t accept more than two columns, we can use a function inside your application to create an ASCII text table from your JSON data and then include that text inside a multiline code block, which should preserve its formatting.
To start, you’ll need a function that turns a JSON array into an ASCII table in whatever programming language you’re working with. The example below is written in JavaScript and takes a JSON object as a parameter and uses the keys as headers for the table columns:
function createAsciiTable(data) {
if (!data || data.length === 0) {
return "No data provided";
}
// Extract headers from the first object's keys
const headers = Object.keys(data[0]);
// Calculate the maximum width for each column
const columnWidths = headers.map((header) =>
Math.max(
...data.map((row) => row[header].toString().length),
header.length,
),
);
// Create the divider
const divider =
"+" +
headers.map((header, i) => "-".repeat(columnWidths[i] + 2)).join("+") +
"+";
// Create the header row
const headerRow =
"|" +
headers
.map((header, i) => ` ${header.padEnd(columnWidths[i])} `)
.join("|") +
"|";
// Generate each row of data
const rows = data.map(
(row) =>
"|" +
headers
.map(
(header, i) => ` ${row[header].toString().padEnd(columnWidths[i])} `,
)
.join("|") +
"|",
);
// Assemble the full table
return [divider, headerRow, divider, ...rows, divider].join("\n");
}
// Example usage
const data = [
{ Month: "January", Savings: "$250" },
{ Month: "February", Savings: "$80" },
{ Month: "March", Savings: "$420" },
];
console.log(createAsciiTable(data));
If you run this function with the data provided, you should receive the output that looks like this:
+----------+---------+
| Month | Savings |
+----------+---------+
| January | $250 |
| February | $80 |
| March | $420 |
+----------+---------+
From there, you can include that text inside of a multiline code block and send the Block Kit payload using the postMessage
API endpoint.
The result looks like this when the table is rendered in Slack:
Here’s an example of how this all gets used together in a Next.js server action:
"use server";
import { postMessage } from "../../lib/slack/postMessage";
function createAsciiTable(data) {
if (!data || data.length === 0) {
return "No data provided";
}
// Extract headers from the first object's keys
const headers = Object.keys(data[0]);
// Calculate the maximum width for each column
const columnWidths = headers.map((header) =>
Math.max(
...data.map((row) => row[header].toString().length),
header.length,
),
);
// Create the divider
const divider =
"+" +
headers.map((header, i) => "-".repeat(columnWidths[i] + 2)).join("+") +
"+";
// Create the header row
const headerRow =
"|" +
headers
.map((header, i) => ` ${header.padEnd(columnWidths[i])} `)
.join("|") +
"|";
// Generate each row of data
const rows = data.map(
(row) =>
"|" +
headers
.map(
(header, i) => ` ${row[header].toString().padEnd(columnWidths[i])} `,
)
.join("|") +
"|",
);
// Assemble the full table
return [divider, headerRow, divider, ...rows, divider].join("\n");
}
// Example usage
const data = [
{ Month: "January", Savings: "$250" },
{ Month: "February", Savings: "$80" },
{ Month: "March", Savings: "$420" },
];
export async function sendTableMessage(channel) {
const tableString = createAsciiTable(data);
const codeBlock = "```$table```".replace("$table", tableString);
const blocks = [
{
text: {
emoji: true,
text: "💰 Our Savings",
type: "plain_text",
},
type: "header",
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: codeBlock,
},
],
},
];
const res = await postMessage(channel, blocks);
return res;
}
Since the createAsciiTable
function automatically appends new line characters to each table row, all we need to do is store that value in a variable called tableString
. From there, we construct a string that will wrap the tableString
value in backticks: "```$table```"
Since the codeBlock
string contains backticks, I decided not to use a
template literal here, opting for the more generalized String.replace
method
you see in the example.
Since this is just a code block, we have a lot more flexibility regarding the size of the table you can upload.
Limitations of ASCII tables in Slack
One thing to be aware of is readability. As you resize the Slack app, view the message on mobile, or if a user views your table in a thread, the table layout may strain against the width of its container.
There is also a character limit for text
fields in Block Kit that is set at 3000 characters, so be aware of that as you develop your applications.
Anecdotally, I also found it difficult to work with the ASCII table in the BlockKit preview environment, since many output methods, like console.log
will render the new line characters.
For most applications that require more than two columns of data, this approach makes the most sense.
Table image as file upload
There’s one more way to render tables in Slack, but it takes a bit more effort.
A few of the other StackOverflow threads on this issue callout uploading the table as an image using the files.upload
endpoint, which is in the process of being deprecated. Slack has introduced a new API endpoint to replace it called files.getUploadURLExternal
that will handle this functionality now.
For certain applications, this may be the best path, but given the changes to the API endpoints, I won’t cover this in detail in this article.
Knock helps simplify building Slack apps
If you're looking to create a Slack app for your product, Knock created SlackKit to help developers simplify the process of creating these integrations.
SlackKit provides you with drop-in components to handle Slack OAuth and channel selection. Those Slack connections are stored in Knock and can be used as a part of multi-channel notification workflows to reach you users where they work.
If you want to add Slack notifications to your application, you can sign up for Knock or reach out to us in our Slack community to chat.