Design Systems: Building a Cross-Functional UI Library with Stencil.js
Kah Yee Kwa
7 mins read
Design systems have been increasingly popular in recent years — and for good reason. In a hyper-competitive tech-driven market, many huge organisations including Google and Apple have created their own design systems that have been used as a guideline for mobile app development. These design systems are fully documented with detailed explanations for building high-quality UI experiences.
So What is a Design System?
In a nutshell, a design system is a collection of reusable design components and assets with guidelines on how to use them while developing a product. These components and assets can be combined or used independently during the development of a product. It also serves as a shared medium of standards and practices between developers and designers.
As the single source of truth, the concept of having a design system is to provide consistent, robust design patterns that can be used by teams to work on products that are aligned with the company’s branding. Teams will have access to the same pre-approved UI components such as buttons and labels that follow the guidelines of the design system.
This set of UI components is also called a UI library. When this UI library is made available to the teams working on new or existing products, the amount of work required, as well as the number of discussions between developers and designers that need to take place before implementing the component, will greatly reduce as it is already handled by the team working on the UI library. As a result, this increases team productivity and creates consistency for their users.
Setting up the Process
Last year, I had the opportunity to work on a UI library for a design system as part of a project to create a revamped internet banking application. As emphasised, the main goal for creating this UI library was to have one consistent design for existing and future projects. Additionally, this project would be the first to implement a standardised design system throughout the bank. Thus, it needed to be able to support multiple frameworks such as ReactJS, Angular and VueJS, as each subproject was unique and might use different frontend frameworks.
With these requirements in mind, my team decided to go with a structure that separated CSS from component logic. In the event of an issue integrating our UI library into the frontend framework, the CSS could be used as a fallback solution for other teams to create their components specific to their framework. Ideally, this should prevent the UI library from being the bottleneck in any project.
My team also decided to use web components to encapsulate the component logic as web component is a defined standard that is included in HTML specifications. This means that it would be natively supported by all modern browsers and compatible with big UI frameworks.
Stencil.js and LitElement are some of the common tools used for building web components for design systems. They are small and lightweight, and using them makes it easier to develop web components as they provide some levels of abstraction such as templates and boilerplate reduction. These tools also combine the best concepts of popular frameworks so it would be easier for new developers to grasp, and for experienced developers who are already familiar with these frameworks to start working on them earlier.
When we were comparing these two tools, my team decided to go with Stencil.js for our web components as it already provided some level of integration with most popular frontend frameworks. Furthermore, as frameworks such as ReactJS and Angular required a certain kind of set up before web components could be used, Stencil.js would also be more suitable as it provided simple configuration to generate framework-specific bundles. All in all, this was in line with our project’s requirements to support multiple frameworks.
In the end, the UI library we worked on provided developers with a set of reusable CSS classes, as well as web components, for use in the development of the new product. This improved development efficiency as it reduced the amount of time needed to style and create the components from scratch.
To give you a deeper understanding of the whole development process, I will be sharing some of the ideas and solutions that my team came up with when we were creating reusable web components with icons for a design system.
When we were building the UI library, one of the components that we had to create was an icon component using SVG assets. We chose SVG instead of icon fonts or images (PNG) as there were several key benefits.
Firstly, it is scalable, which means that we do not need to manage the quality of each icon in order to suit different screen resolutions. For example, a Macbook with Retina display has a higher resolution compared to a Windows laptop with the same screen size. This will cause the image to look blurry when it is displayed on the Macbook. Compared to font icons or image icons (PNG), SVG is vector-based and increasing the size of SVG icons will not degrade its quality. Another benefit of using SVG for icons is that it is easily animatable. Designers can create the animated SVG asset using tools like Photoshop or Sketch and export it for developers to use.
Below are some of the ideas that my team explored, the problems we encountered, and how we overcame them while creating the icon component:
SVG has a very powerful element called <use>. It can be used to reference external resources and render it in the Document Object Model (DOM). This is especially useful when we want to have a SVG sprite that can be reused throughout a web application. It would look something like this in native HTML:
As this uses a reference instead of inlining the SVG in HTML, we can keep the source SVG icon in an assets folder as a .svg file and use it elsewhere in the code. We can also modify or replace the SVG icon without affecting any of the codes that are referencing it. As compared to inlining SVG on every page that uses it, this method would be following the DRY concept.
DRY stands for “Don’t Repeat Yourself”, which means to avoid redundant repeated codes. This concept is beneficial because there will only be one source of truth, similar to why the UI library was created. Any changes to that source will propagate down to all the references without going through the entire codebase and this makes coding more efficient.
Using this method also has other benefits such as leveraging the browser’s cache. If the SVG has been requested from the server before, the browser will automatically store it in its cache (memory) and serve it from cache when requested again without hitting the server. This will improve application performance, saving both bandwidth and time.
Here is an example of the web component in Stencil.js using the SVG <use> tag:
Naturally, there are a few complications. Firstly, this method is not supported on IE. This will be a problem if your application needs to work with IE. That being said, it was not a problem for us as IE has since been deprecated and our business team decided not to support it (so a win for developers!)
Note: We’re not required to support IE for this project so SVG <use> is still a viable option if you want to use our UI library’s icons.
Secondly, when a consumer uses our UI library, they would normally install them in a `node_modules` folder. As this folder only exists during development and not available when the codes are live in production, and the `xlinkHref` needs to be referencing the `node_modules` folder, this will cause problems when serving the application on a browser to the users. To solve this, consumers will have to manually copy the assets from the UI library and place them in the application folder. This is not ideal as the consumer will have to keep track of changes to the SVG icons while updating the UI library. We need it to be transparent for the consumer when they are using our UI library’s icon component.
Taking Advantage of ES6 Import
As mentioned, the previous method will not work in a UI library because it is not ideal for consumers to go through the hassle of maintaining the SVG assets that are coming from our library. To solve this issue, we found a solution that would allow our web components to internally import the SVG assets and serve them directly from the component. This is where ES6 import comes in.
Below is an example of importing the SVG asset `import svgIcon from ‘../../../css/assets/sad.svg’;` from the assets folder and relying on Stencil.js build tool to transpile the icon to a Base64 format.
The “@internal” annotation is there only to mark this component as internal and not generate readme.md files.
Note: This is an undocumented feature and may not work. However, it worked for us.
As seen from the code above, we needed to create a component for each SVG icon and import the asset from the source folder. The other option would be to create a component that uses dynamic import to import SVG on runtime. At runtime, this would cause issues as the assets would be located in different folder locations as compared to during development, resulting in an empty element in the HTML. Also, we want to keep things as simple as possible.
When generating the output bundle, the SVG icon was transpiled into Base64 encoding as expected below:
As you can see, the imported SVG icon was converted into a Base64 encoded string and encapsulated in the same component file. This was beneficial as it was still cacheable by the browser and also takes advantage of Stencil.js’ lazy loading.
After integrating this into our test ReactJS application, it had seemed to be the way forward. Since we had a huge list of SVG icons to generate, we invested some time into automating the process of generating the icons component with a script before typing a simple command, which I will not go into detail.
But alas, although this looked like a feasible solution, we encountered our next problem: the Safari browser prevented unsafe Base64 strings from loading. This meant that any consumer application running on Safari would not be able to see SVG icons. We were forced to find another solution.
Converting Base64 into Inline SVG
The problem we encountered in Safari was because of the Base64 formatted string. This was the default behaviour of Stencil.js’ build tool and there was no straightforward way of configuring it. Hence, we needed to find a way to replace the Base64 format.
After some research, we came across a rollup plugin that was written specifically for Stencil.js, which converted Base64-formatted strings into inline SVG on build time. Using this tool would be beneficial for us as it would optimise the SVG files when we bundle our UI library for consumer use. Even though this plugin was useful to a certain extent, it did not tick all our boxes. Since SVG files could come from anywhere in the web and in our case, from tools used by the design team, they may contain redundant codes that will increase the file size and affect load time.
By taking inspiration from this plugin, we modified the plugin to include SVG Optimiser (SVGO) https://github.com/svg/svgo. SVGO is a great tool that optimises SVG files.
With all these changes in place, the result was a success:
The `svgIcon` in the bundle was now an inline SVG!
To further simplify the usage of our UI library, we created a wrapper component that took in the icon name and internally rendered that icon’s web component. Here’s an example of the wrapper component:
In summary, design systems are fast becoming a key differentiator in business strategy as user demand for better experiences has forced companies to rethink how they deliver products.
As part of a project to build a UI library for a design system, we managed to keep the requirements of having a separate CSS for styling while maintaining a web components UI library by leveraging multiple tools such as Stencil.js.
This separation allowed us to cater to most if not all of the frontend UI frameworks as having a CSS fallback would allow any framework to implement their own version of an unsupported component using the CSS provided. This also enabled us to work on improving support for that framework without blocking any project progress.
I hope this article provides some helpful insights for anyone else facing similar challenges, and empowers teams to continuously ideate and discover your own ways of navigating problems to build one-of-a-kind digital experiences.
Note: This article is written with the following dependencies: