Engineering

Engineering nits: Building a Storybook for Slack Block Kit

We care a lot about the pace of shipping at incident.io: moving fast is a fundamental part of our company culture, and out-pacing your competition is one of the best ways we know to win.

In engineering teams, one way to ship fast is to invest in tools that make your team more productive. We've become good at identifying small pains and frustrations that slow us down over time and – after surfacing them to the rest of the team – find solutions for them.

This is the first of a series inspired by our #engineering-nits Slack channel where the team share friction points. It doesn't take long for messages to turn into PR's and this series is where we'll share what we did: big, or small.

We have a lot of Slack!

Our product is an incident response platform where our customers – often – start and respond to an incident all from within Slack.

This means a load of our product needs to be exposed in Slack, where we:

  • Listen for commands (e.g. /inc to declare an incident)
  • Provide Slack forms to collect input (e.g. asking for incident details in response to an /inc command)
  • Post messages back into the incident channel with buttons (e.g. an announcement post for an incident with an “escalate” button)

For those unfamiliar with Slack apps, you build these visual experiences using Slack Block Kit a design system that renders UIs from JSON structures.

Slack have a great tool called the “Block Kit Builder” that provides a wysiwyg editor where you can build forms or messages then share a link so others can see your work. It’s great for getting design feedback – see below for an example of our “declare an incident modal”.

…maybe too much Slack.

“Awesome!”, I hear you say: Slack’s given us everything we need to develop these applications, you must be having a great time.

Well, kinda.

Block Kit Builder is awesome but our app is growing pretty big. In fact, we have over 70 different Slack modals (forms) and almost as many messages now, which is really quite a lot to keep track of.

And so we arrive at the nit:

Block Kit Builder is great, but at our scale, we need something more.

If you’ve worked in frontend you may have heard of Storybook. What Storybook allows you to do is write example code alongside frontend components and expose those examples a web UI. It’s a game-changer for documenting design systems or allowing quick previews of whole page layouts.

It seems obvious that this is what we want for our Slack Block Kit: an indexed, easily shareable collection of our Block Kit snippets which is always kept up-to-date with our code, and allows seeing each form/message in its many variations.

Collecting example Block Kits

Ok, we know what we want – a Block Kit Storybook-esque browser – now we just need to make it.

All our Slack modals are built with a common ‘framework’ where you implement an interface, name your modal, then register it with the framework so it becomes useable/routable.

For these two example modals:

The code that implements them looks like this:

Both files call modal.Register inside an init function (Go runs these on package initialisation) at the top of the file, telling our framework that this modal exists and how to render it.

As every modal calls Register and we’d like every modal to be exposed in our eventual library, it makes sense to change Register to ask not just for the modal but also for examples of how this modal might be built.

We can see how this looks for the escalate modal:

modal.Register(func(render func(modal EscalatePagerDuty) *slack.ModalViewRequest) slackpreview.Template {
	return slackpreview.Template{
		Name:        "Modal: Escalate: PagerDuty",
		Description: "Escalate to PagerDuty",
		Variants: []slackpreview.Variant{
			{
				Name: "default",
				BlockKit: render(
					EscalatePagerDuty{
						Incident: fixtures.IncidentStandard,
					},
				),
			},
			{
				Name: "private incident",
				BlockKit: render(
					EscalatePagerDuty{
						Incident: fixtures.IncidentStandard.Apply(
							domain.IncidentBuilder.IsPrivate(true),
						),
					},
				),
			},
			{
				Name: "unusable",
				BlockKit: render(
					EscalatePagerDuty{
						Incident: fixtures.IncidentStandard,
						IntegrationDisabled: true,
					},
				),
			},
		},
	}
})

This will activate the escalate modal and build three ‘Slack previews’ for it: one for a normal incident, one for a private incident (we need to warn people before they page people about something sensitive!) and another for when the account doesn’t have a usable PagerDuty connection.

The slackpreview package that owns the Template type tracks all these examples, and the modal.Register function adds them into it.

Rendering “Slack previews”

So we’ve stored our example Block Kits, but how do we render them?

The answer is to repurpose Slack’s official Block Kit builder: the web UI that allows you to build Block Kit fragments. One of its most useful features is sharing your Block Kit by links, which it provides by serialising the entire Block Kit body into the URL.

This is great news for us: if we can render Block Kit URLs for each of our examples, people can easily open each example in the builder.

So we do just that:

While not as fancy as Storybook, all we need is an HTML page that contains links to the Block Kit builder for each of our modals and their variants.

So we hooked up the slackpreview package to an API endpoint and used a small Go template to render that index page. It’s pretty big so we provide a table-of-contents with internal fragment links to jump around, but it does the job, and does it well.

Just click each variant to open Block Kit in a new tab.

Feels good

It’s such low effort creating these examples that it’s cost effective to require them, meaning every Slack modal or message in our app can be opened in the Block Kit builder within seconds, even by non-engineers.

That’s a massive win for productivity, and it’s hard to imagine building Slack interfaces without them now.

Picture of Lawrence Jones
Lawrence Jones
Product Engineer

Operational excellence starts here