Headless Components
What they are, Why they're Beneficial, and How to Incorporate them
Headless components are a trend that I've seen emerge over the past couple of years. With veteran libraries like Downshift and ReachUI and newer libraries like Radix and Headless UI, it seems to be a trend that is here to stay. Throughout this article, I will provide a more thorough definition of what headless components are, why they are beneficial, and how they can be incorporated into your component libraries.
What are Headless Components?
Headless components are unopinionated, unstyled, components that handle a majority of the tricky implementation details so that you can build out components faster. Headless components do not have preset styles but instead provide full, interactive functionality for a particular component pattern.
Benefits
Built-in Accessibility: Creating accessible components that meet the WAI-ARIA standards and properly handle aspects like keyboard navigation can be a large undertaking. Headless component libraries aim to simplify this often difficult process by providing a set of flexible and extensible components that are prebuilt with accessibility and developer experience in mind.
Reusable & Flexible: Headless components are typically flexible enough to be able to handle most business requirements and implementation details. Because these components don't apply any preset CSS, they can make great candidates for component libraries as you can tailor and style them to meet most design systems. In addition, headless components tend to handle things like keyboard interactions and focus management providing one less thing for developers to have to worry about.
Inversion of Control: Headless components put a majority of the power in the developer's hands via "inversion of control". This pattern obscures enough of the implementation details and provides you with an API that you can control and style to meet most needs.
"With great power comes great responsibility": As with most things in software development, there is a trade-off to using headless components as it does put more responsibility in the developer's hands when it comes to styling and certain rendering logic. However, I personally think the benefits outweigh the cons especially when it comes to implementing tricky component patterns for non-native elements like date pickers and autocomplete inputs.
Headless Component Libraries
This section will contain a list of some of the most popular component libraries available on the market today. Some of the options are better suited for certain scenarios but I've provided a variety of choices so that you can choose the library that works best for your use case.
1) Headless UI
Headless UI is built by the Tailwind Labs team making it an obvious option for projects that use Tailwind CSS. The library provides components for both React and Vue making it usable across multiple JS frameworks. When building out my RoadTrip.FM project I reached for the Listbox
and Disclosure
components. I've found that Headless UI coupled with Heroicons, another open-source project by the Tailwind Labs team, make for a great combo!
Headless UI Tabs Component Example:
import { Tab } from '@headlessui/react'
function HeadlessUITabs() {
return (
<Tab.Group>
<Tab.List>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>Content 1</Tab.Panel>
<Tab.Panel>Content 2</Tab.Panel>
</Tab.Panels>
</Tab.Group>
)
}
2) Reach UI
Reach UI has been around for a while and is one of the O.G's in the headless component library space. Reach UI seeks to become the accessible foundation of your React-based design system and has solid accessibility support as it claims that each component is tested with Safari + VoiceOver, Firefox + NVDA, and Edge + JAWS. Reach UI was initially created by Ryan Florence and Michael Jackson, the duo behind Remix and React Router.
Reach UI Tabs Component Example:
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@reach/tabs";
function ReachUITabs() {
return (
<Tabs>
<TabList>
<Tab>One</Tab>
<Tab>Two</Tab>
</TabList>
<TabPanels>
<TabPanel>
<p>one!</p>
</TabPanel>
<TabPanel>
<p>two!</p>
</TabPanel>
</TabPanels>
</Tabs>
);
}
3) Radix
Radix Primitives is the library’s flagship product and is described as a collection of “unstyled, accessible components for building high‑quality design systems and web apps in React.” Instead of reinventing the wheel, Radix Primitives handle a lot of the tricky parts when it comes to building out common component patterns.
They already have a reputable number of primitives and have plans to add to that list. I've written about Radix before in my "Building a design system with Radix" article for LogRocket and I found the documentation and developer experience to be quite good.
Radix Tabs Component Example:
import * as Tabs from '@radix-ui/react-tabs';
export default () => (
<Tabs.Root defaultValue="tab1">
<Tabs.List aria-label="tabs example">
<Tabs.Trigger value="tab1">One</Tabs.Trigger>
<Tabs.Trigger value="tab2">Two</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1">Tab one content</Tabs.Content>
<Tabs.Content value="tab2">Tab two content</Tabs.Content>
</Tabs.Root>
);
4) React Aria
React Aria is built by the team at Adobe and uses a hook-based approach to implementing components. According to their documentation:
React Aria is a library of React Hooks that provides accessible UI primitives for your design system. It provides accessibility and behavior for many common UI components so you can focus on your unique design and styling. It implements adaptive interactions to ensure the best experience possible for all users, including support for mouse, touch, keyboard, and screen readers.
Check out the useTabList
hook 🪝 to see how to implement tabs using React Aria.
5) Reakit
Reakit is a lower-level component library for building accessible high-level UI libraries, design systems, and applications with React. Reakit strictly follows WAI-ARIA 1.1 standards, is customizable, and is built with composition in mind.
Reakit Tabs Component Example:
import { useTabState, Tab, TabList, TabPanel } from "reakit/Tab";
function ReakitTabs() {
const tab = useTabState();
return (
<>
<TabList {...tab} aria-label="My tabs">
<Tab {...tab}>Tab 1</Tab>
<Tab {...tab}>Tab 2</Tab>
</TabList>
<TabPanel {...tab}>Tab 1</TabPanel>
<TabPanel {...tab}>Tab 2</TabPanel>
</>
);
}
Honorable Mentions
The items in this section are additional libraries that can be leveraged that also use a "headless" approach to their implementations.
1) Downshift 🏎
Downshift is a great choice if you need an autocomplete, combobox, or a select input in your application and you want it to be accessible and flexible. The library provides a robust API that leverages interesting patterns like render props and state reducers. The library initially created by Kent. C Dodds provides a nice solution for these notoriously hard to implement components.
2) MUI Base
MUI Base is built by the same team behind MUI (the artist formerly known as Material UI). According to the MUI Docs:
MUI Base is a library of unstyled React UI components. These components were extracted from Material UI, and are now available as a standalone package. They feature the same robust engineering but without implementing Material Design.
MUI Base includes prebuilt components with production-ready functionality, along with low-level hooks for transferring that functionality to other components.
With MUI Base, you can rapidly build on top of our foundational components using any styling solution you choose—no need to override any default style engine or theme.
At the time of this writing, MUI Base is still in alpha which is why it's included in this section and not part of the larger component library list.
El Fin 👋🏽
Headless components are a relatively new pattern that has been gaining popularity in the frontend space and I'm loving the flexibility, extensibility, and accessibility benefits that this pattern provides. I highly recommend trying out a headless component library the next time you have a use case for them.
If you enjoy what you read, feel free to like this article or subscribe to my newsletter, where I write about programming and productivity tips.
As always, thank you for reading, and happy coding!