I had the pleasure of leading a team of frontend engineers to build an internal library of reusable and accessible UI components based on our design system. We had to implement over 40 UI components and each needed to meet acceptable standards for accessibility, reusability, and maintenance. The UI library needed to cater to the needs of both legacy products that are based on Vue 2 and more recent products that are based on Vue 3.
In this article, I would like to share the lessons I learned from that experience and the decisions and tradeoffs we had to make. Here is a summary of the lessons I learned from building an internal library of design system components:
- Build on top of open-source, headless UI libraries when you can.
- Prioritize web accessibility.
- Be guided by a set of software engineering best practices.
- Define and document a component’s architecture before development.
- Test components in real-world usage, not just in isolation.
- Create well-written documentation for developers on how to use the new UI library.
Now let’s get into the details!
Build on top of open-source, headless UI libraries when you can.
Building design system components is not an easy task, especially when you consider accessibility requirements and some edge cases in functionality. This is why building on top of open-source libraries that have taken care of important aspects, such as functionality and accessibility, would be a great way to start. Popular libraries such as Radix UI, Headless UI, and PrimeVue are top of the list, at least in the Vue ecosystem. These libraries provide ways to customize the theme and styles of components using your brand colors, typography, etc., and get accessibility and functionality for free.
However, my team and I did not build on top of a headless UI library. Instead, we built the components from scratch. One thing that all popular and actively maintained open-source headless UI libraries, such as Radix-UI, Headlessui, and PrimeVue, have in common is that they only support Vue 3. They don’t support Vue 2.
I pointed out earlier that some of our products are legacy systems based on Vue 2. So to address this challenge, we first built components in Vue 2 to support the legacy apps, then created a Vue 3 version of the same component to support the Vue 3 apps, ensuring that both variants share the same API. This consistency in API made it easy to write one documentation that supports both the Vue 2 and Vue 3 variants of the same component. Also, since engineers can be rotated across different product teams, consistency in API ensures that engineers don’t have to learn a new API when working on a product that uses the Vue 3 or Vue 2 components, which is great for developer experience.
JavaScript libraries such as floating-ui and tippy.js were very helpful in building components such as tooltips, popovers, and dropdowns. Packages such as tabbable and focus-trap-vue were helpful in implementing a keyboard-accessible modal.
Building on top of an open-source headless library would have been a great choice. However, in software engineering, it’s important to understand fundamentals and learn how things work under the hood so that you can still get the job done when there is a good reason to build from scratch.
In hindsight, probably a much better approach would have been to develop the component library as web components by leveraging frameworks such as Lit Elements or Stencil.js. That way, instead of maintaining two variants of the same component, we would only have one library of web components that can be used across products irrespective of the JavaScript framework the products are built with. From my experience, components built with Vue.js or React.js make it easy to craft a better developer experience compared to web components. However, software engineering is mostly about trade-offs, and by allocating more time for development, it’s possible to build a robust UI library of web components that can serve the needs of enterprise applications.
Prioritize web accessibility.
In addition to writing semantic HTML, one of the easiest ways to build an accessible application is to ensure that the UI building blocks are accessible. This is why accessibility is at the core of any open-source UI library. If an open-source UI library does not prioritize accessibility, the web community will frown at it. This is why Figma sites have been getting a lot of criticisms related to accessibility violations. Despite how important and challenging building complex accessible experiences can be, you get accessibility for free if you leverage standard open-source UI libraries, as mentioned earlier.
Since we built our components from scratch, that made implementing accessibility even more challenging and interesting. To help my team get accessibility right, I shared this resource that contains accessibility guidelines for building many design system components.
Also, courses on frontend masters related to accessibility were very helpful. If you are interested, check out Enterprise Web App Accessibility and Web Accessibility, v3.
You can leverage arc toolkit chrome extension to perform accessibility testing. You can press CMD + Shift + C
on a Mac or Ctrl + Shift + C
on Windows to quickly inspect accessibility properties of an element.
The takeaway is that when building UI components, prioritise accessibility, not just aesthetics. The web community rewards what works for all, not just what looks good.
Be guided by a set of software engineering best practices.
When building a project, it's necessary for the codebase to be guided by a set of software engineering best practices. Below are some of the guiding principles I emphasised before we commenced development.
- Atomic design: Components were designed in a modular, reusable fashion, breaking down complex components into smaller, manageable building blocks. For instance, below is a sample usage of our Tabs component.
<SfeTabs default-tab="tab1" variant="shadowButton">
<SfeTabList>
<SfeTabTrigger value="tab1">Tab 1</SfeTabTrigger>
<SfeTabTrigger value="tab2">Tab 2</SfeTabTrigger>
<SfeTabTrigger value="tab3">Tab 3</SfeTabTrigger>
</SfeTabList>
<SfeTabContent value="tab1">
<p>Content for Tab 1</p>
</SfeTabContent>
<SfeTabContent value="tab2">
<p>Content for Tab 2</p>
</SfeTabContent>
<SfeTabContent value="tab3">
<p>Content for Tab 3</p>
</SfeTabContent>
</SfeTabs>
I spent time going through Chakra UI and Shadcn to get inspiration on how to compose complex components. Vue’s provide/inject played a critical role in allowing us to break down components such as Tabs, Accordion, Breadcrumb, Dropdown, etc., into a set of related, tightly coupled components.
- Single responsibility principle: Each component had a clear, focused responsibility, ensuring clean and maintainable code.
- Reusability: Components were built to be reused across different projects and contexts, reducing redundancy and effort. I recommend this course by Michael Thiessen on reusable components.
- Declarative programming: We focused on describing what the UI should look like rather than how to build it, making the code easier to reason about and understand.
Define and document a component’s architecture before development.
Before developing a new component myself or assigning a task to an engineer, I documented the component’s architecture on Notion to specify its usage, props, slots, custom events, and accessibility criteria. It made development easier, as my team and I could discuss about a component’s API and make necessary adjustments before development.
For example, this is a section of a draft of the Select component architecture
Test components in real-world usage, not just in isolation.
One of the lessons I learned the hard way was the importance of testing components both in isolation and in real-world usage to see how a component integrates with other components.
For example, in a real-world application, the Select component will be used inside a form alongside other components such as Input, Checkbox, Radio buttons, etc. While each of these components should be tested in isolation, they should also be tested in the context of how they will be used together in production.
Testing the Select component in isolation would be fine. But testing it inside a form can let you know if the component works well with form validation rules. For instance, does clicking the submit button trigger an error state on the Select component if no option has been selected from its dropdown menu despite the validation rule specifying that at least one option must be selected?
Also, testing the Select component inside a modal can help you test that its dropdown menu is not confined to the height of the modal.
Testing in isolation reveals bugs; testing in integration reveals design flaws. So it’s important to test in both contexts.
Create well-written documentation for developers on how to use the new UI library.
Creating well-written documentation is as important as developing the components. Engineers should not have to study the source code just to learn how to use your components.
To improve adoption and developer experience, we created documentation that showcased example usage of each component. We also created an API reference section for each component, highlighting its props, slots, and events. We were able to create one documentation since both the Vue 2 and Vue 3 variants of each component exposed the same API.
I used prism.js to create a basic syntax highlighting component and copy-to-clipboard to implement a ‘copy code to clipboard’ functionality.
/*
Takes the 'language' and 'code' props and returns the same code with syntax higlighting, with support for HTML (markup), JavaScript and CSS.
*/
highlightCode() {
if(this.language === 'markup') {
return Prism.highlight(this.code, Prism.languages.markup, 'markup');
} else if(this.language === 'javascript') {
return Prism.highlight(this.code, Prism.languages.javascript, 'javascript');
}
return Prism.highlight(this.code, Prism.languages.css, 'css');
Conclusion
Leading development of 40+ reusable and accessible components for use across both legacy and modern codebases was a demanding and rewarding experience. At the core of our decisions were accessibility and developer experience. Prioritizing accessibility from day one, treating documentation as a first-class deliverable, testing components both in terms of isolation and integration , and maintaining a unified API across both the Vue 2 and Vue 3 components were critical to the success of the project.
Top comments (0)