Have you ever run into the issue of elements looking different from page to page, or had multiple teams implementing the same design in their own ways? Have you experienced your styles breaking after updating a dependency, and you had no way to find out before the changes made it into production? Have you wasted time asking around for mockups because you weren’t sure what a certain component should look like?
In this article, we will talk about how we at Forto were able to overcome these kinds of challenges through the use of a design system.
A design system is a collection of guidelines which describe how different elements such as text, inputs, and buttons should look and behave, ensuring consistency in the look and feel of the application. While the idea is certainly not new, the rise of component-based frameworks such as React and Vue, along with tools like Storybook, has enabled designers and developers to move beyond static PDF files and images into live documentation with code samples and the possibility to interact with elements. This makes collaboration easier and allows for more code reusability.
Early on at Forto, we were having difficulty maintaining consistent styling across all our applications. At that time we were using the popular Material UI library with some customization, but did not have a standard design reference. Also, we had adopted a monorepo approach, where our frontend code (apps and common components) would be in the same repository. While it might sound good in theory, in practice it led to slow releases and introduced a lot of coupling between different parts of our system. This resulted in a user interface that was disjointed and rather unappealing, and a developer experience that was less than ideal.
Building the foundation
We were introduced to the concept of design systems by one of our product designers. He set out to create a document to define the layout, colors, typography, and iconography which we would use as a foundation for redesigning our applications. This provided the engineering team with the perfect opportunity to revisit our approach for implementing and sharing components. It soon became clear that keeping the design system within the monorepo would only hinder its development, so we decided to start anew and publish it as a NPM package from a different repository. This allowed it to grow independently, and also gave us the chance to add useful tools such as ESLint and Dependabot, which were not easy to integrate with the existing codebase.
In order to develop components without any applications to test them in, we needed a sort of sandbox where we could visualize them and quickly iterate as needed. This is where Storybook came into play. Not only did Storybook enable us to build and interact with our components in isolation, but it also gave us the ability to easily document them and display examples of how they can be used. With the knobs feature, we could see in real-time how changing a certain property would alter the component’s appearance. Other add-ons allowed us to check how well the component complied with accessibility standards, or theme the documentation with the company’s brand colors and custom icons. Combining these with automatic bundling and deployments from our CI/CD pipeline made our components much more visible and accessible to our designers and all the developers in our tech team.
Leveling up with visual testing
One thing we deemed important while developing our design system was robustness. Normally, components are tested according to how they should behave (e.g. clicking a dropdown should open a menu). However, in the case of a design system, it is also important to make sure that the styles match our guidelines. We wanted to avoid situations where a component’s appearance changes without our knowledge, possibly causing some bugs in our applications. To do so, we employed a technique called visual regression testing, where we let the computer compare two images to find any discrepancies.
For this, we use a Storybook add-on called storyshots-puppeteer, which allows us to take image snapshots of our example pages and store them inside the repository. Whenever we create or update a component example, we update our snapshots, which makes the visual changes explicit during the code review. Our CI/CD pipeline also runs tests which compare the snapshots generated by the pipeline-built storybook with the ones saved in GitHub. This is especially useful for checking pull requests from tools like Dependabot or Renovate which automatically update dependencies. For example, a newer version of Material UI increased the spacing between the text field and the error message below it. Thanks to our automated visual regression tests, we were able to catch this.
However, there were some limitations with this approach. The human eye might not be able to detect a difference of a few pixels, but machines certainly can. This is an issue because we found that snapshots generated by one engineer’s MacBook were different from those generated by another, and worse, they were both different to the one generated by our CI/CD pipeline, which uses Linux virtual machines. This disparity could be accounted for by setting a minimum threshold percentage (say 1%) when comparing how many pixels do not match between two images. If the percentage difference is below the threshold, then the computer can consider the two images to be the same.
While this worked pretty well, having a threshold could cause us to miss some actual regressions. We believed that there was a way to achieve a 0% threshold, i.e. exact comparisons. The solution we came up with was to containerize the snapshot generation and testing process using Docker. By using the same environment to render the storybook in both the developer’s machine and the CI/CD pipeline, we could ensure that the generated snapshots would be exactly the same and whatever discrepancy is detected is caused by an actual change.
Reaping the benefits
The design system has been a game changer for us. Although there were some pain points initially with the migration and it took us a while to roll it out completely, the rewards have easily been worth the time and effort we invested. In the two years since we’ve had our design system, it has helped us tremendously in a number of ways. It keeps the look and feel of our applications consistent, and speeds up our frontend development efforts because we no longer have to create a lot of custom styling.
Another advantage is increased collaboration and less duplication. What one team builds, another can use, and engineers across the entire tech organization share the responsibility of developing and maintaining the library. Our hosted storybook documentation makes it easy for newcomers to get familiar with all the components we work with. Finally, our automated visual regression tests allow us to effortlessly keep our design system up-to-date without sacrificing robustness, giving us full confidence as we move forward with Forto’s vision to “make shipping products as easy as sending emails.”
Don’t forget to keep an eye on our Forto Career pages — we are expanding, and we would love you to join us.