Contentful's Forma 36 design system, which we've been working on since 2018, is used by internal teams to create the UI of our products and it's used by external developers to create apps for Contentful. Today, it boasts over 13,000 weekly downloads, meaning it's no longer just a small project.
And recently, we released version 4 which has already half the downloads of the previous version. I'm working on the design system every day and I can honestly say I'm really proud of it.
To get the best out of the new design system version, we decided to focus on user feedback and interviewed our customers to get insights on our components.
We heard specific feedback around the components and their usability, but surprisingly, we discovered that our documentation would also need an update. The three main complaints were:
Many code examples did not reflect the everyday use cases.
Some of the examples were broken. They couldn't be copied and pasted.
Users needed a place to try out the components and their props before using them.
We went back to the drawing board and decided to take a fresh take on documenting our design system. Let me share with you what we came up with.
Writing docs isn't fun, but it can be better
One obstacle in the way of improved documentation was time to build. Our site was built with Gatsby and it generated pages from README files in our repository. Unfortunately, we faced long build times after every made change in any of those files.
The team and I prefer to work on component codes over writing the docs. Imagine having to wait for long builds on top of that. The team lost motivation fast. Therefore we knew that we needed to improve our doc's developer experience first.
So we took two days to build a Next.js prototype that would generate pages from the same README files and compare it with our Gatsby setup. The difference was huge! The prototype was only taking seconds to re-generate a page after any change. Besides that, with Next.js we can have different rendering strategies for each page of our website which means that some pages could be static, some could be rendered in our servers, etc. Endless possibilities for the future came up!
With this, we were ready to tackle our users' problems.
How to evaluate component use cases
The problem of not having real-world use cases in our documentation looked easy at first. We would only need to copy the usage of our components that we already have in our products and apps. Even though that's true in theory, it was a very slow process to find cases in our codebase that were simple enough to teach our users how to use our components.
And also, how many examples should we add to the documentation? Could we cover all the possible cases?
Our approach was to find the usage cases with less business logic around them. Let's take the Autocomplete component as an example. In the old website, we showed an autocomplete that selects fruits for a shopping list.
That's not a real case, and it took the component out of context. In Contentful's web app, we use the autocomplete component as a "Space" selector.
So we replaced the "Shopping list" example with the "Space selector" example to reflect what we use our components for in Contentful.
Before, we had just examples where an array of strings was passed to the autocomplete:
However, in "real life," the data passed to the autocomplete is never just a set of strings. It's always a set of objects with several properties and the user will search among those objects based on one of their properties. So we added examples using arrays of objects.
Lastly, during the development of the website, an external user approached us for help building an autocomplete that would request async data to get its suggestions. While complex, we thought it was a case worth sharing, so we added an example as well.
In the end, we've introduced a significant number of examples. We've also learned that our search for use cases will never be finished. As the community approaches us with problems that need to be solved, we hope to update our documentation to include them. If you have something to share, feel free to reach out to us on slack org (#forma-36) or open an issue in our GitHub repository.
With more use cases in our tool belt, we moved to the next challenge: generating useful examples and correcting those that were were broken.
Avoiding broken examples
In the old website, we recorded examples in markdown, as if they were just another part of our content. The problem here, as you might be aware, is that it's easy to make mistakes when writing code. Component examples are easy to overlook in code reviews and are, as a result, subject to breaking.
For this to work, we moved all the examples to TS files that we store in an "examples" folder in each component package. This code is not published to npm, so you don't download it when installing Forma 36 to your project. But we add the path of those files in the README of each component. This way Next.js can pick the code and render it on the page.
Now, if we make a mistake in the code of the example, ESLint will warn us, and the TS compiler will not work. We get errors before publishing any changes to our users.
Now that we drastically overhauled our examples, how could we make them more interactive and fun?
Enabling experimentation with a new playground
While we were working on redesigning the website, CodeSandbox released Sandpack, which is a library to create live coding environments that they developed while building their product.
We were already using CodeSandbox to aid with community support. We previously shared bug reports and recommended implementations with a CodeSandbox link.
With the introduction of Sandpack, we can use the power of CodeSandbox within our documentation website. And if the React team adopts SandPack, why shouldn't we do the same?
SandPack comes with some handy React CMS components which we could quickly implement in our Next.js application. If you're also looking to implement interactive code examples to your design systems docs, here are the detailed steps we took.
Setting up SandpackProvider
The first thing we did was set up the SandpackProvider component.
It would've been enough to add just React packages and the three Forma 36 packages (f36-components, f36-icons, and f36-tokens), but use other dependencies in a couple of our examples, so we had to extend the list them:
The next thing we did was define an index.js file passed to the provider. Sandpack uses this file content to initialize the preview. As you can see, the only thing it does is render a React app with Forma 36 GlobalStyles:
Here, we saw an error in the preview area because React failed to import our app component from App.js because it didn't exist yet:
We also passed a small styles.css file to SandpackProvider to add padding to the preview window:
The last thing we passed to the SandpackProvider was the code we wanted to preview and edit. Here's the string that we passed as the App.js file:
The value of the code prop here came from a query parameter in the URL (more on that later), but if the parameter had been undefined we would have passed this string as the default value:
That fixed the error message shown before and returned a code like this:
We were amazed at how straightforward it was to create this feature. We got a great in-browser code editing experience with just a few components and props.
Sharing code between pages
Here's something else that's cool! Open this URL in another tab, then come back.
Did you see the following?
This was possible because the code is in a query parameter in the URL. The playground gets that parameter and then shows it in the editor and in the preview.
Why did we do that? Well, we wanted every code example on the website to have an "Open in playground" button. This would enable users to jump to the playground from any example that they wanted to explore in more detail.
But for that to be possible, we had to find a way for a component page to share its code example with the playground page.
The most direct route to achieve this was to compress the code of the example and use it as a query parameter. For example, here's what the "Open in playground" button opens up to look like:
And in the playground page, we decompress this parameter and pass it down to SandpackProvider as a prop. This is where this happens:
By the way, this is also what makes it possible for our users to share links to their creations in our playground. The code is in the URL.
Previewing and editing code
Only setting a SandpackProvider isn't enough. We need to be able to edit the code and see a preview of it. For that, we set up both SandpackCodeEditor and SandpackPreview as children of SandpackProvider:
(The SandpackLayout component is just positioning the editor and the preview side-by-side)
We passed a couple of props to those components, but they are just our preference of setup. Sandpack gives us a bunch of props to make the code editor and code preview look like we want. You can learn what each prop does in their documentation.
That's it! We now have a playground to experiment with Forma 36 directly in the documentation.
We all know that proper and extensive documentation is the marker of a solid software library. But creating good content and automating it poses a challenge.
To help things along, a good development experience is essential. Otherwise, it's easy to demotivate the team maintaining the library. Investing some time to migrate our old website to Next.js was essential for this. If we hadn't done so, we would have had a hard time updating our examples and content.
We also believe that interactive documentation is the way to go moving forward and the team at CodeSandbox enabled us to build a great live code feature in our design system documentation. With this feature, it's not only easier to discover component configuration and properties, but our community members can share components with a single URL.
If you maintain a documentation library, we highly recommend using Sandpack. P.S. CodeSandbox, you rock!