Building a React Component Library - Part 1
Introduction
When it comes to React component libraries, there's no shortage of excellent options to choose from. Popular libraries like material-ui
, react-spectrum
, fluentui
, and polaris
have already earned their place as favorites among developers. Nevertheless, as I plan for an upcoming website project, I can't shake the idea of creating my own library from scratch. In this blog post, I'll explain how and why I made certain technical decisions for my new library, wtlin-ui
.
These decisions include choosing Emotion as the style engine, implementing theme-based system CSS props and sx
Prop with the Styled System library, and providing theme colors via CSS custom properties to enable Color Mode without the dreaded flash of unstyled content.
Choose Emotion
as the style engine
When it comes to styling frontend applications, there are several popular options available, including CSS, SASS, CSS-in-JS, and zero-runtime libraries. As I considered my choices, I narrowed my focus to two CSS-in-JS libraries (styled-components and Emotion) and two zero-runtime libraries (Linaria
and vanilla-extract
).
Out of these four options, styled-components
is undoubtedly one of the most well-established and widely used libraries, with extensive documentation. However, I ultimately decided to use Emotion
instead. Emotion
has a smaller bundle size and offers superior performance and more flexibility, making it the ideal choice for my project. In fact, the Storybook and MUI teams both recommend using Emotion
for various use cases.
Linaria
was another library that caught my attention. This zero-runtime CSS-in-JS library extracts CSS to standalone CSS files during build time, eliminating the need for runtime CPU overhead. This approach also offers caching benefits and makes it possible to deduplicate styles using Atomic CSS CSS. However, the lack of top-notch documentation and dedicated website, combined with the trial-and-error process of finding information, ultimately led me away from choosing this library.
Finally, I considered vanilla-extract
, a modern solution with excellent TypeScript integration and no runtime overhead. While its minimal features, straightforwardness, and opinionated nature appealed to me, the fact that it processes everything at compile time and generates static CSS files was not enough to overcome the significant downside of code co-location.
Ultimately, after careful consideration, I decided to go with Emotion
for its superior performance, flexibility, and overall suitability for my project's needs.
Add theme-based system CSS props and sx
prop with Styled System
System CSS properties and the sx prop are becoming increasingly popular. Many frameworks, like material-ui
, support them nowadays. I personally use them as they simplify components and prevent developers from jumping back and forth to check trivial layout styles.
Writing code like this whenever you want to retrieve values from the theme can become tedious:
The theme-based Style Props
Style functions of styled-system
will try to find a value from the theme object, even for deeply nested values, and fallback to a hard-coded value if they can't.
The sx
Prop
Define the type SxProp
and the style function sx
:
Next, combine it with styled
API provided by emotion
(or styled-component
):
Now, the Box
component can accept sx
prop and be used like this:
While the sx
prop is powerful, it's important not to abuse it. Here are some guidelines:
-
Use the
sx
prop for small stylistic changes to components. For more substantial changes, consider abstracting your style changes into your own wrapper component. -
Avoid nesting and pseudo-selectors in
sx
prop values when possible.
The System CSS Props
Suppose I want my Box
component to accept not only the sx
prop but also the space
and typography
CSS props. In that case, it can be done easily with styled-system
:
Now, my Box
component can accept spacing and typography props like this:
Enable Color Mode with CSS custom properties
The renderToString
method renders a React tree to HTML string, including the styles() that are also rendered to string on the server-side. However, as there is no way to know the user's preference for color mode (apart from using a session), styles() are rendered with the default color mode, which is typically the light mode. This can result in two significant problems:
- the mismatching of content during hydration
- the flash of incorrectly styled content (which may not be critical but can still be problematic)
Fortunately, these issues can be resolved by taking advantage of CSS custom properties and an inline script.
CSS-in-JS without CSS custom properties
server client
│ │
│ /home │
┌───┤◄────────────────┤
│ │ │
│ │ │
SSR └──►├────────────────►│
(light mode) │ ├───┐
│ /bundle.js │ │ Construct DOM
│◄────────────────┤ │
│ │◄──┘
├────────────────►│
│ ├───┐
│ │ │ Construct CSSOM
│ │ │
│ │◄──┘
│ │
│ ├─ ─ ─ render page with ─ ─
│ │ light mode │
│ ├───┐ │
│ │ │ Execute JS │
│ │ │ hydrate with Flash!!
│ │ │ dark mode │
│ │ │ (mismatch ERROR ❌)│
│ │◄──┘ │
│ │ ▼
│ ├─ ─ ─ re-render with ─ ─ ─
│ │ dark mode
│ │
│ │
│ │
─┴─ ─┴─
CSS-in-JS with CSS custom properties
server client
│ │
│ /home │
┌───┤◄────────────────┤
SSR │ │ │
│ │ │
└──►├────────────────►│
│ ├───┐
│ /bundle.js │ │ Construct DOM
│◄────────────────┤ │ Execute inline script
│ │◄──┘
├────────────────►│
│ ├───┐
│ │ │ Construct CSSOM
│ │ │
│ │◄──┘
│ │
│ ├─ ─ ─ render page with ─ ─
│ │ dark mode │
│ ├───┐ │
│ │ │ Execute JS │
│ │ │ hydrate with No Flash!!
│ │ │ dark mode │
│ │ │ (no ERROR ✅) │
│ │◄──┘ │
│ │ ▼
│ ├─ ─ ─ re-render with ─ ─ ─
│ │ dark mode
│ │
│ │
│ │
─┴─ ─┴─
Styled System with CSS custom properties
To make the theme clean and avoid being messed up by CSS custom properties, I created two helper functions: flatWithPath
and toCssCustomProperties
.
With these helper functions, I can define the theme in the same way as before:
The flatWithPath
function is used to apply the styles to the HTML using CSS custom properties, while the toCssCustomProperties
function enables the use of styled-system
with CSS custom properties.
Here's an example of how these functions can be used:
With these functions, the theme is kept clean and separate from the use of CSS custom properties, making it easier to maintain and modify the theme as needed.