The Code Etymologist is a multi-part series that covers typical software knowledge that deserves to be documented and shared within development teams.
- Preface
- Part 1: Project setup
- Part 2: Coding guidelines
- Part 3: Development workflows
- Part 4: Product requirements
- Part 5: UI Components library
- Part 6: Data structures
- Part 7: Technical decisions
- Closing thoughts
Nowadays, we don't have to rebuild reusable UI components from scratch. There are plenty of libraries and frameworks out there, both free and premium, that provide a wide range of commonly used UI components: Tailwind Plus, Radix, shadcn/ui, Headless UI, Material UI, Chakra UI, Ant Design, Bootstrap, and many more.
However, we often need to customize them, creating our own wrappers. Most of the time, we only need to customize the styles, but sometimes we also need to extend or change their behavior. Additionally, we often need to build our own project-specific components.
As a team member, new or old, we might have a few questions, especially regarding the customized and project-specific UI components:
How can we discover the components library? If they are groupped together inside a single folder or extracted in a separate repository, then it's trivial to discover them.
What can they do? Declaring a typed interface is useful to discover their capabilities, to prevent using invalid input, and to aid during refactorings.
What do they look like and how do they behave? The behavior aspect of UI components can be covered by automated tests. However, the visual aspect can only be consumed with our own eyes.
Now, imagine looking for a UI components library that answer only the the first 2 questions. Imagine they only display the list of components and their public interface, without showing us how they look and behave. We would have to install the library and play around with the input to discover the visual output. It's tedious. To be honest, no serios library would put their potential users through this experience.
Interactive playground
Since UI components are tightly coupled with the visual aspect of the project, it makes sense to document them accordingly. That's the reason why any popular UI library offers interactive documentation.
There are several open source tools that offer the possibility to document our own UI components, however I would like to highlight Storybook simply because it's feature-rich and framework agnostic.
There are plenty of real-world examples on their showcase page that you can play with. One you might be familiar with is the VSCode UI Toolkit.
- we can browse the UI components:
Button
,Panels
,Radio
, etc; - we can see what they can do, for instance, Buttons can have Default or Secondary appearance, can have Autofocus, can be Disabled, or can include Icons;
- we can see the public API, the inputs/props, and their types;
- we can change the inputs using the auto-generated controls;
- we can see, and edit the code that produces an actual output.
These are all built-in and out-of-the-box features. Additionally, there are plenty of addons that we can use, such as Light/Dark theming, Viewport controls, Accessibility checks, and many more.
This kind of documentation is valuable for developers and designers, as well. You can read more about isolated components driven development which I wrote about in 2021 and it's still relevant today. It's an approach to UI components implementation, useful not only for documentation purposes, but also during development and debugging.
Automated tests
While component stories are great for visualizing and experimenting with UI components, complex behaviour cannot be easily documented using this approach. This is where automated tests come into play.
There 2 major categories of UI component tests when it comes to rendering:
- Rendering on an emulated DOM;
- Rendering on a real DOM using a headless browser;
They both provide benefits and downsides, so let's take a brief look at each rendering approach.
Emulated DOM
Most unit testing frameworks, like Jest or Vitest, execute the tests on Node.js by default. Since Node.js doesn't know anything about the DOM, which is part of a client's browser, we can configure the testing framework to use an emulated DOM instead, such as jsdom or happy-dom.
Testing Library provides helpful utilities for rendering, interacting, and asserting UI components. It supports both emulated and real DOM implementations.
👌 BenefitsThe biggest advantage of using an emulated DOM is the high execution speed and the lack of dependency on a real browser.
👎 DownsidesThe downside of emulated DOMs is that they don't support all the features of a real browser. For instance, we cannot perform bounding box measurements, manipulate scrollbars, or subscribe to scroll events.
Real DOM
The alternative to emulated DOM is to use a real DOM from an actual browser. End-to-end frameworks, such as Playwright or Cypress also provide isolated UI components testing, integrated with a real browser. Additionally, Storybook also provides component testing.
This approach is similar to unit testing, considering that the framework renders a single UI component automatically in a browser, headless or not, without requiring us to build the application and provide a working URL, as we do in the case of E2E tests.
👌 BenefitsThe biggest advantage of using a real DOM is full support for standard browser features. There is no limitation here.
👎 DownsidesHowever, having access to a real browser comes with a cost. Since there are many browsers available, some exotic features might work only in some of them. We also need to start the browser and communicate with it during tests, which is typically slower. Last, but not least, the overall complexity of the whole setup is higher compared to the simpler Node-based emulated DOM solution.
To conclude, the visual aspect and the behaviour of UI components are complementary. Interactive playgrounds document how the component should be used and how it looks. In contrast, automated tests document and assert behaviour, providing a strict control mechanism.
Continue reading Part 6: Data structures