Headless components in React and why I stopped using a UI library for our design system

Nir Ben-Yair
10 min readFeb 15, 2022

--

In the past five years I’v been working on major frontend projects at Gloat. In this blog post I’ll focus on what I learned when building Gloat’s design system and how I moved from using UI libraries like MUI to using headless components.

When building our design system, I had to answer these requirements:

  • Accessibility: the components must be accessible.
  • Theming: each component should support multiple themes (light mode\dark mode for example).
  • Uniqueness: The look of our product should be unique. We don’t want our product to have that Material (or Bootstrap) generic look and feel. We have a design team, and they determine how our product should look like.
  • Browser support: it should support all major browsers and IE11 (Yeah, don’t even get me started on this one).
  • Functionality: it should support Gloat’s own unique use cases — We need complete control on how our components behave.
  • Responsiveness — it should support all screen sizes and devices.
  • Maintainability: it should be easy and seamless to modify and maintain.

My Material UI & React-Bootstrap Journey

When I first joined Gloat we used react-bootstrap. We used it only for the more complicated components and not for atomic components like Button, Checkbox and Avatar, which we implemented ourselves.
But as I got better with React, I felt that MUI had a much better and modern component API, so after a gradual six months effort we were able to replace Bootstrap with MUI in all of our applications and in our design system.

Things were going great! I liked the way MUI supported theming, their customization mechanism and the way they used all modern React best practices like hooks, dot notation sub components (<Menu.Button />) and Context.

I will build my own Material UI

After a while, our app started growing and we needed to implement more complicated UI components that will support our unique use cases. I’m talking here about components like Autocomplete, Combobox, multi-tag-select, Dropdown and Modals. Each one of them had functionality that was unique to our product and their own custom design.

In our Combobox component, which is basically a multi-select with an input to allow filtering, I needed to implement specific design requirements and to support some custom functionalities. For example: I needed to keep the dropdown open when selecting some items, but to close it when I click others. I tried to force MUI to play nicely with our design and functionality requirements, but soon faced a wall. Reading about others on the internet trying the same, I realized my problems were not just my own.

I spent a lot of time hacking my own needs on MUI components and while I managed to do so, it felt dirty and complicated. At the end of the day, I didn’t feel content with the code I wrote and came to a conclusion that it will be hard to maintain and scale in the future.

So after a few struggles I decided to build those components myself.
I wanted total control over functionality and design, and my code to be clean and open to changes. That was a huge mistake.

The result was the code ended up being long, complex and hard to understand. But that wasn’t the only problem: I started getting bugs from users using IE, Safari and some mobile devices that weren’t tested properly. I’m talking here about “Can’t select a dropdown item on some iOS devices on Safari (BUG-34939)” kind of bugs. The biggest problem though was accessibility. My hand-made components were simply not accessible enough for keyboard users and people who use screen readers. The only benefit I got from building those myself was complete control over functionality, markup and design.

I gave up, and said to myself: “well, stupid me, let’s go back to MUI, I guess people use it for a reason”.

A slide from Pedro Duarte’s talk at Next.js conf in 2021. Pedro is one of the creators of Radix UI, and here he describes how much time it took them to implement a fully accessible dropdown menu that works well in all browsers and supports all screen readers.

Headless components — How do they help?

But then I saw a question on the Israeli React Facebook group -

“What is the best UI library to use with React?”

Honestly, that question comes up every month or two and it tends to bring a lot of emotions from us frontend developers. I read different opinions until I saw a comment that caught my attention. A group member called Nick Ribal mentioned that he never uses UI libraries and always builds his own components by hand. I knew this guy from the FB group and remembered that he knows what he’s talking about so I had asked him: “how do you handle all the complicated components like autocomplete, combobox, multi-select and menu dropdowns? Those are super complex, need keyboard navigation, specific markup and need to work well everywhere”.

I was asking it because of my own bad experience of implementing these components myself. I now know that building a good autocomplete, that works well in all browsers and is accessible, can take months. I have tried that and failed.

Nick said that he uses “headless components” to solve that problem and told me to check out the Reach UI library documentation site. I entered their website and learned that Reach UI provides components that are in charge of accessibility and functionality but leave the entire design for you to handle yourself. I thought the concept was interesting but I wasn’t impressed enough. I still thought — “I’m not sure how that might help me or how it can be better than MUI which everyone uses and is heavily tested”.

Side note: I think that the very old-fashioned design of the Reach UI website made me go away and not take this library seriously. A month ago, when I interviewed Kent C. Dodds on my podcast, He told me that the authors of Reach UI, which are also the authors of Remix (and react-router), actually came up with the idea for Remix when they started rewriting the entire Reach UI website. I’m happy that they’ve understood that their doc site needs some improvements :)

Headless components — A whole new world

At that point in time, I started using Twitter. I won’t get into details about how just being on Twitter made me a better developer, but while I was scrolling through my feed, I started reading about this new and exciting library called “Headless UI” — A UI library for React that provides accessible components but without the styling — so you can style them however you’d like.
I entered their website (which is beautiful!) and started learning about their approach. After that I went into their Github repository, started to read their code, and loved it! So I decided to refactor our menu-dropdown component to use Headless UI. After a couple of hours I was able to create an accessible menu component with my own styling and actually replace all instances of it in our product and design system.

The first version of the menu built with headless ui, no styling at all, but we do get the open\close functionality and focus management out of the box.
A snapshot of the DOM output of headless ui’s menu. We can see that the markup is valid and includes accessibility attributes like role, tabIndex and aria-haspopup.
The final version of our Menu, with just a little bit of CSS

Headless UI’s components are well tested on multiple browsers, platforms, and devices and deals with edge cases that I never could or want to deal with myself: stuff like focus management, keyboard navigation, event listeners, accessibility attributes, valid markup and screen reader support. I felt like I finally understood what Nick was talking about.

So I started reading more about headless components in React, and finally figured them out. The concept is pretty basic: The libraries will give you well tested and accessible components or hooks without any default styling, so you can style and render them however you’d like, and if the authors were kind enough — you’ll also be able to control their functionality and behavior.

Researching headless components, I found that there are a couple of popular libraries:

  • Radix UI: My favorite. Well tested, accessible, and has one of the best component API-s I’ve seen. It does have some problems when testing it’s components with React Testing Library, and does not support IE very well.
  • Reach UI: pretty good and reliable, from the authors of react-router and Remix. Not a lot of components, but covers most use cases.
  • Headless UI: a small number of components. Well tested, work best with Tailwind CSS. Only drawback is that it’s not that easy to change the functionality and behavior of the components.
  • Downshift: By Kent C. Dodds. Focuses on autocomplete, select, combobox, and multi combobox components. Well tested, accessible, and gives you total control over styling and functionality. I chose it for our combobox and autocomplete components, and learned a lot from their implementation.
  • React-aria: By Adobe. I don’t know too much about it, but it looks interesting.
  • Reakit
  • Ariakit

While those are large libraries, there are many single-component packages out there. Those only focus on one type of component: headless modal, headless pagination, headless radio button and so on. One of my favorites is react-table by Tanner Linsley, which is a headless… table.

The Combobox example

This is an example of our combobox component that was written using Downshift. Our combobox has some unique functionalities and it’s own design. Implementing this design and my needed functionality with MUI is possible, but much more complicated and convoluted. I have to go against MUI, while headless components are designed for precisely that. By using Downshift, I created an accessible combobox that is well tested with all screen readers, has keyboard navigation, and holds most of the logic this type of component needs. And all that in a couple of hours! In addition, because we control the rendering logic, we can actually render the markup however we’d like and add any other markup or functionality ourselves. Here I combined it with the react-virtual library for virtualization of long lists.

In this case headless components gave me the best of both worlds: They are accessible and reliable as MUI’s components, but I have total control over styling and functionality.

A naive example of the combobox component. The headless “component” is just a “useComobox” hook that returns prop getters we can pass to our own markup. The props returned from the “getInputProps” method will take care of all focus management and interaction with the input while typing, so we won’t need to implement those complex functionalities ourselves. Since this hook is not in charge of styling, we can style the component however we’d like.

Bundle size

Another benefit of using headless components is that they don’t bloat my final bundle. Since I’m importing only the components/hooks I need, I know the final bundle will get the bear minimum without any other code that always comes along when installing a major UI library.

Styling

When it comes to styling, headless components will work well with any styling solution. If you’re into Tailwind CSS, just use their classes on the markup. If CSS-in-JS is your thing — you can easily style them with styled-components, Emotion, vanilla-extract or Stitches. You don’t have to use the UI library’s own customization mechanism, which are usually complex and often coupled to the library’s specific implementation.

Cons of using Headless components libraries

  • More control but also more responsibility — Headless components give us more control but that comes with the tradeoff of us needing to make more decisions along the way. Since headless components are usually in charge only of accessibility and functionality, we still need to implement the styling and most of the rendering logic ourselves. We will even have to make more UX decisions when using them because they are not very opinionated about that.
  • Community — UI libraries like MUI, Bootstrap or Ant Design have large communities behind them. You can always find solutions to your problems on their Github issues page, and bug fixes are published on a regular basis. While headless libraries are also backed by strong communities, they’re not as nearly as big or active as the major UI libraries ones.

When to favor UI libraries over headless components?

When writing this post I wanted to find some drawbacks for using headless components and my first thought was that using them over UI libraries might hold you back if you need to ship a product very quickly. After all, rendering an autocomplete from a UI library is simple and more intuitive, we just need to render an <Autocomplete />, pass it some props and we’re done. But from my experience with headless components, implementing the same component using a headless library doesn’t take much longer, and gives you much more in the long run.

So my current answer to this question is that if I know that I’m building a design system that has unique functionalities and a custom design, I will definitely go with headless components. I might choose a UI library if I’d feel I can answer most of the requirements without putting too much effort in customizing it’s components.

In my specific use cases I needed to support functionalities and design that were very unique to our product and implementing those with a UI library wasn’t worth it.

Conclusion

Since discovering and embracing headless components, I’v found that I don’t need nor want to use a UI library in projects where I have a very unique design requirements. Using headless component has done wonders to our developer experience, user experience and to our relationship with the design team.

Another benefit we got is that it improved the way we write React components in general — separating logic and functionality from the UI.

My recommendation is to get familiar with the concept of headless components and see if it can fit your project’s use case.

Watch my talk about headless components from the ReactNext conf June, 2022:

Also, feel free to follow me on Twitter. @nirbenya

--

--