Base UI Combobox

At the time of writing this, the current version of Base UI is
1.0.0-beta.2
and an official combobox component is still in development.
Over the past week, I’ve been trying out Base UI, a collection of headless UI components from the creators of Radix, Floating UI, and Material UI. I’ve used a few headless component libraries, like Headless UI from Tailwind CSS, Reach UI from Ryan Florence and Radix by way of shadcn/ui. Each of these have had their pros and cons. It’s difficult to find a library of components that can toe the line between opinionated and customizability. You’re never going to make everyone happy.
I really enjoyed working with Popper and followed it over to Floating UI, which I currently use every day. But, Floating UI is on the customizable side of the spectrum, which means it can come with a pretty dense amount of boilerplate.
While trying out Base UI and swapping out the Tooltip and Dialog components on this site, I noticed that Base UI didn’t have a combobox component yet. But, they do have an Input and a Select component. So I figured, why not try to Frankenstein together a passable combobox?
Design Spec
The UX for this is heavily based on Headless UI’s Combobox. Some of those design decisions are as follows:
- No error states, an invalid input reverts to an empty state.
- Pressing the
down arrow
should change focus from the input to the options list. - Pressing the
up arrow
while highlighting the first option should return focus to the input. - Pressing
escape
mid-input returns to an empty state. - Pressing
enter
with an input that matches an option label (regardless of casing) sets that option as the selection.
Demo
Process
One of the challenges was not having access to the active item state to control which select options were highlighted and when. This meant I needed to do some ugly DOM tree climbing to check data-highlighted
and data-selected
props. There was also some fighting with the select component’s management of that state and some awful setTimeout
functions that run clean-up. Maybe there’s a way to access additional state. I didn’t dig much deeper than the docs to look for a context or clever use of render props. These are very much “me” problems and not a knock on Base UI.
All this is to say, the code to make this work is not great. But, the end result is not bad. Honestly, I wouldn’t think twice about using this in a production app. The benefits that come with Base UI, even in beta, far exceed anything I could build on my own from scratch.
Code
I’ve thrown the code on CodeSandbox for anyone to fork. Maybe don’t look too closely.