If you were roaming around in the JavaScript ecosystem this year you might have come across this interesting UI library called shadcn/ui. Instead of being distributed as a npm package, the shadcn/ui components are delivered through a CLI that puts the source code of the components into your project itself. The creator mentions the reason for this decision in the official website for shadcn/ui,
Why copy/paste and not packaged as a dependency?
The idea behind this is to give you ownership and control over the code, allowing you to decide how the components are built and styled.
Start with some sensible defaults, then customize the components to your needs.
One of the drawbacks of packaging the components in an npm package is that the style is coupled with the implementation. The design of your components should be separate from their implementation.
In essence, shadcn/ui is not just another component library but a mechanism to declare a design system as code.
My intention with this article is to explore the architecture and implementation of shadcn/ui to review how it has been designed achieve the before mentioned goals.
If you haven't tried out shadcn/ui yet I would suggest visiting the shadcn/ui docs and play around a bit with it to get the most out of this article.
Any user interface can be broken down into a set of primitive reusable components and their compositions. We can identify any given UI component to be constituent of its own behavior set and a visual presentation of a given style.
Except from purely presentational UI components, UI components should be aware of user interactions that can be performed on them and should react accordingly. The foundations necessary for these behaviors are built-in to the native browser elements and available for us to utilize. But in modern user interfaces we need to present components that contains behaviors that cannot be satisfied by native browser elements only. (Tabs, Accordions, DatePickers etc.) This warrants the need to build custom components that looks and behave as we conceptualize.
Building custom components is usually not difficult to implement at the surface level using modern UI frameworks. But most of the time these implementations of custom components tend to overlook some very important aspects of the behavior of a UI component. This includes behaviors such as focus/blur state awareness, keyboard navigation and adhering to WAI-ARIA design principles. Even though behaviors are very important to enable accessibility in our user interfaces, getting these behaviors right according to W3C specifications is a really hard task and could significantly slow down product development.
Given the fast moving culture of modern software development, it is difficult to factor in accessibility guidelines into custom component development for front-end teams. One approach a company could follow to mitigate this would be to develop a set of unstyled base components that already implements these behaviors and use them in all projects. But each team should be able to extend and style these components effortlessly to fit the visual design of their project.
These reusable components that are unstyled but encapsulate their behavior set are known as Headless UI components. Often these can be designed to expose a API surface to read and control their internal state. This concept is one of the major architectural elements of shadcn/ui.
The most tangible aspect of UI components are their visual presentation. All Components have a default style based on the overall visual theme of the project. The visual elements of a component is two-fold. First is the structural aspect of the component. Properties such as border radius, dimensions, spacing, font-sizes and font-weights contributes to this aspect. The other aspect is the visual style. Properties such as foreground and background colors, outline, border contributes to this aspect.
Based on user interactions and application state, a UI component can be in different states. The visual style of a component should reflect the current state of the component and it should provide feedback to the users when they interact with it. Therefore different variations of the same UI component should be created order to accomplish this. These variations, often known as variants are built by adjusting the structure and visual style of a component to communicate its state.
During the development lifecycle of a software application, a design team captures the visual theme, components and variants when developing high-fidelity mockups for the application. They would also document different intended behaviors of components as well. This type of a collective design documentation for a given software is usually known as the Design System.
Given a design system, the responsibility of a front-end team would be to express it in code. They should capture the global variables of the visual theme, reusable components and their variants. The main benefit of this approach is that any change done in the future to the design system can be efficiently reflected in code. This would unlock a friction-less workflow between the design and development teams.
As we have discussed before shadcn/ui is a mechanism by which design systems can be expressed in code. It enables a front-end team to take a design system and transfer it a format that can be utilized in the development process. I think that the architecture that enables this workflow is worthy of our review.
We are able to generalize the design of all shadcn/ui components to the following architecture.
shadcn/ui is built on the core principle that states The design of your components should be separate from their implementation. Therefore every component in shadcn/ui has a 2 layered architecture. Namely,
Structure and behavior layer
Style layer