Lessons learned while maintaining Contentful's design system

Illustrated graphic of a person standing in front of documents under a bell jar
Published
February 22, 2022
Category

Developers

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.

GIF of Contentful's Forma36 autocomplete

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.

GIF of Contentful's 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:

const spaces = [
    'Travel Blog',
    'Finance Blog',
    'Fitness App',
    'News Website',
    'eCommerce Catalogue',
    'Photo Gallery',
  ];

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.

Example GIF of autcomplete with async data

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.

To prevent this, we figured we should use the same tools for this project that we use to prevent bugs when developing other features (i.e., a linter and a compiler). At Contentful, we use ESLint and the Typescript compiler. Even though our examples are not written in TS, writing them and passing them through the compiler makes sure that they will work in a plain Javascript environment as much as in a Typescript one.

Screenshot of autcomplete working in JS

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.

Screenshot of Sandpack and Contentful's autcomplete function

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 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:

<SandpackProvider
  customSetup={{
    dependencies: {
      react: '^17.0.0',
      'react-dom': '^17.0.0',
      'react-scripts': '^4.0.0',
      '@contentful/f36-components': '^4.0.0',
      '@contentful/f36-tokens': '^4.0.0',
      '@contentful/f36-icons': '^4.0.0',
      emotion: '^10.0.17',
      lodash: '^4.17.21',
      'react-hook-form': '7.22.5',
      'react-icons': '4.3.1',
      'react-focus-lock': '^2.5.2',
      'react-sortable-hoc': '^2.0.0',
      'array-move': '^3.0.0',
    },
  }}
>

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:

const indexFile = `import React, { StrictMode } from "react";
import ReactDOM from "react-dom";
import { GlobalStyles } from "@contentful/f36-components";
import "./styles.css";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <GlobalStyles />
    <App />
  </StrictMode>,
  rootElement
);`;

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:

Screenshot of error in the preview area

We also passed a small styles.css file to SandpackProvider to add padding to the preview window:

const stylesFile = `
  body {
    padding: ${tokens.spacingM};
  }
`;

<SandpackProvider
  customSetup={{
    files: {
      '/styles.css': {
        code: stylesFile,
        hidden: true,
       },
       '/index.js': {
         code: indexFile,
         hidden: true,
       },
     },
     dependencies: {
       react: '^17.0.0',
       'react-dom': '^17.0.0',
       // …
    },
  }}
>

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:

<SandpackProvider
  customSetup={{
    files: {
      '/App.js': code, // <- Here's the code we want to show
      '/styles.css': {
        code: stylesFile,
        hidden: true,
       },
       '/index.js': {
         code: indexFile,
         hidden: true,
       },
     },
     dependencies: {
       // ...
    },
  }}
>

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:

import { Button } from '@contentful/f36-components';

export default function App() {
 return <Button>Click on me!</Button>
}

That fixed the error message shown before and returned a code like this:

Screenshot showing that the error message is fixed

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?

Screenshot of thank you message

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: 

/playground?code=COMPRESSED_CODE

And in the playground page, we decompress this parameter and pass it down to SandpackProvider as a prop. This is where this happens:

export default function Playground() {
  const [code, setCode] = useState('');

  useEffect(() => {
    const query = qs.parse(window.location.search.slice(1));
    const decoded = tryDecode(query.code);
    setCode(decoded ?? defaultCode);
  }, []);

  if (!code) {
    return null;
  }

  return <SandpackRenderer code={code} />;
}

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:

<SandpackLayout>
  <SandpackCodeEditor
    showTabs={false}
    showLineNumbers
    showInlineErrors
    wrapContent
  />
  <SandpackPreview
    showSandpackErrorOverlay
    showOpenInCodeSandbox={showOpenInCodeSandbox}
    showRefreshButton
    viewportSize="auto"
  />
</SandpackLayout>

(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.

Conclusion

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!

With that, we hope you try our new Forma 36 documentation and share your feedback in our slack channel. We're eager to make it even better.

About the author

Don't miss the latest

Get updates in your inbox
Discover new insights from the Contentful developer community each month.
add-circle arrow-right remove style-two-pin-marker subtract-circle