Skip to main content

Annamalai

Let me tell you a real experience where I felt the need for headless UI components around 2-3 years ago. I was working on an application and we had a good set of basic UI components with strict typography and color system. The most common UI components were buttons, form elements, modals, toasts, etc.., As days passed by, we needed a date picker to satisfy our app needs and so we decided to rely on a third-party date picker to not slow down our development process. The product team had a set of peculiar requirements for the date picker and we started looking for a date picker that suited our needs based on these requirements. To give a brief on the tech side, the app was using Ember.js as the client-side framework and an internal atomic CSS library like Tailwind.css for building most of the UI in the app. So, out of all the date pickers, there was the only one which met all our needs and some of the requirements that is on top of my head are that it should allow selection of date range, give time inputs, have predefined date ranges, show date picker only when custom range option is selected, buttons to save changes in some cases and other cases it should autosave, etc.., We were quite happy to find one which suited our requirements and the team added the package as a dependency and happily started using it in all the places. When we integrated into our app, the designers noticed and without any second thoughts, gave feedbacks to change the UI of the date picker because it was inconsistent and was not following the existing UI guidelines.

The original UI provided by the date picker library

Original Date picker

The expected date picker UI which designers wanted

Expected Date picker

All well and said, changing the date picker UI to our needs was a total nightmare. Let me tell you why...

  1. The UI of the date picker was built on top of Bootstrap.css.
  2. The icons used by the date picker were from some font icon library that loaded all the icons though it used only a few of them.
  3. The internal UI of the date picker was a total black box in terms of styling customization like the layout.
  4. It added a whopping 20+ kb of CSS to the vendor bundle and used libraries like Popper.js for dropdown placements (though we had our own dropdown, it was of no use in this case).

To meet the design feedback of the designers, we had to accomplish most of it only using CSS.

Bootstrap.CSS dependency

To remove Bootstrap.CSS from the dependency and also to design based on our existing color system, we used a trial and error approach to remove the unused CSS and assign appropriate CSS color and font values to the CSS class to align the date picker UI to our needs. Believe me, it sounds easy but it really is not. Though our team preferred using atomic CSS, we had no other go but to write an additional set of styles and had to always keep an eye on this to keep it aligned with our UI style guidelines.

Font Icon

We removed the font icons from the dependency and achieved consistency with some CSS hacks. The font icon was replaced with base64 encoded SVG icons from our icon design library in the CSS file. Whenever the designer changes the icon, we manually had to update the styles with new base64 SVG icons.

Date picker Layout Changes

As I said, it was a total black box, customizing is highly impossible. We again resorted to CSS hacks to hide unnecessary button options and overall, it was like doing CSS Zen Garden to achieve our layout but in a limited DOM scope. This added a lot of CSS hacks.

Transitive Dependencies

We were able to remove dependencies like Bootstrap.css, font icons but removing libraries like popper.js was highly impossible and it became a necessary evil.

So, now the question to you fellow developers is

  1. Do you think this approach is maintainable or scalable?
  2. Do you think this approach is easier to update as the UI guidelines change?
  3. When a designer comes in with a new requirement, do you think will you able to achieve it without much difficulty?
  4. Is the code clean enough, so that other developers in the team can contribute?

Ok, to me, this code is a horrible and total nightmare to maintain and in most cases, in a team of say 20+ developers, only 1 or 2 might have intermediate knowledge on how it works.

Solution

From my past experience, I have come to a conclusion that the only solution that works very well to solve these kinds of problems to build or use a headless UI component. So, What is a headless component?

A headless user interface component is a component that offers maximum visual flexibility by providing no interface.

By Merrick Christensen

Yes, a headless component doesn't provide any UI but provides all the functionalities which can be used to build a meaningful and highly customizable user interface. Good examples of headless UI components are Kent's Downshift, Zendesk's react-container, React Table, etc.., And note that it is not something specific to React and it can be achieved in other popular frontend frameworks also,

React - It might look something like this

const { getters, handlers } = useHeadlessDialog();

<div
{...getters.getDialogAttributes({ accessibilityLabelledBy: "dialogTitle" })}
{...handlers.addFocusLock()}>
<header>
<Heading id="dialogTitle">I am a Dialog Title </Heading>
<Button
{...handlers.closeMouseHandler()}>
<Icon name="close" />
</Button>
</header>
</div>;

Vue - Using scoped slots we can achieve the headless approach (for detailed stuff, check out Adam's post and Dillon's post)

<HeadlessDialog>
<template slot-scope="{ getters, handlers }">
<div
v-bind="getters.getDialogAttributes({ accessibilityLabelledBy: 'dialogTitle' })"
v-on="handlers.addFocusLock()">

<header>
<Heading id="dialogTitle">I am a Dialog Title </Heading>
<Button v-on="handlers.closeMouseHandler()">
<Icon name="close" />
</Button>
</header>
</div>
</template>
</HeadlessDialog>

Ember - It can achieved using a combination of template yield, modifiers and helpers.

<HeadlessDialog as |getDialogAttributes addFocusLock closeMouseHandler| >
<div {​​​​{bind attributes=(invoke getDialogAttributes 'dialogTitle') handlers=addFocusLock}​​​​​​} >
<header>
<Heading id="dialogTitle">I am a Dialog Title </Heading>
<Button {​​​​​​{on 'click' closeMouseHandler}​​​​​​}>
<Icon name="close" />
</Button>
</header>
</div>
</HeadlessDialog>

The headless UI component approach does add some verbosity but it can be overcome by using the technique used in Zendesk's design system. The idea is quite simple, make a headless component and also a base component built on top of the newly created headless hook or render prop component. A simple example for a Switch component would be something like below,

Now if in the future, there comes a new requirement and if it is not achievable by the base component, we can always resort back to the headless component to build a new one to achieve our requirements.

As I said, it does add some verbosity to the system, so following this approach is not advisable for all cases. For example, in an internal design system, since we know the requirements, we can build a component that achieves our needs. This headless component approach works best when you want to publish an open-source UI of maximum flexibility and also in other extreme cases.

Thank you, reader!