Tags: web-dev ux concept

Headless Components

Date: 2026-08-16


Behaviour, state and accessibility with no styling at all. The pattern won because the hard part of a combobox was never the CSS — and styling is the part every team wants to own.


A headless component provides a component’s logic — state management, keyboard interaction, focus handling, ARIA attributes — while rendering no opinion about appearance.

WHAT IT GIVES YOU        WHAT IT DOESN'T
open / closed state      colours
keyboard navigation      spacing
focus trap and return    typography
ARIA roles and ids       layout
click-outside            animation
typeahead

Why the split is at the right place

The costs of building an interactive component are wildly asymmetric:

A COMBOBOX

styling        a few hours, and every team
               wants it different anyway

behaviour      weeks, and you will get it
  ├ arrow key navigation
  ├ typeahead matching
  ├ focus return on close
  ├ aria-activedescendant wiring
  ├ id generation and association
  ├ screen reader announcements
  ├ click-outside and Escape
  └ scroll-into-view
                              wrong

Almost every hand-built select, modal and tab set in production has accessibility bugs, because that list is long, invisible when broken, and only discovered by people using assistive technology — Screen Readers, Keyboard Navigation.

Headless libraries let you buy the expensive half and keep the cheap half, which is exactly the right trade.

What it looks like

<Disclosure>
  {({ open }) => (
    <>
      <Disclosure.Button className="my-btn">
        Delivery options
      </Disclosure.Button>
      <Disclosure.Panel className="my-panel">
        Next-day available.
      </Disclosure.Panel>
    </>
  )}
</Disclosure>

The library supplies aria-expanded, aria-controls, the id wiring, the toggle state and the keyboard handling. Every class name is yours.

Where it fits in a design system

The layering that works:

HEADLESS LIBRARY     behaviour + a11y
        ↓
YOUR COMPONENTS      tokens applied,
                     API constrained
        ↓
PRODUCT CODE         composes yours

Do not expose the headless library directly to product code. Wrap it, apply your tokens, and narrow the API to your system’s conventions. Otherwise every consumer restyles independently and you have a component library that guarantees nothing — Component API Design.

The trade

HeadlessFully styled library
Visual controlTotalFight the defaults
Time to first screenSlowerFast
AccessibilityHandledHandled
BundleSmallerLarger
Upgrade riskLower — no CSS to breakHigher

Headless is the right default for a product with its own brand. A fully-styled library is right for internal tools where nobody is paid to care what it looks like, and for prototypes.

The honest cost: you still have to design and build every state. Headless gives you no starting point, and a team without design capacity will produce something worse than the styled library’s defaults — Component States.

  • Render props / children-as-function — the mechanism above for exposing state to markup you control
  • Hooks — the same logic with no markup at all: useCombobox() returns props to spread onto your own elements. Maximum control, most assembly
  • asChild — merging behaviour onto an element you supply — Polymorphic Components

These are three points on one line: how much markup does the library own? Hooks own none, render props own the structure, asChild owns the props.

Where it goes wrong

  • Treating it as free accessibility. The library handles the widget; it doesn’t handle your contrast, your focus visibility, your labels or your content order. Those remain yours — WCAG
  • Not wrapping it, so the system’s consistency guarantee never exists
  • Under-styling. A headless component with minimal CSS often ships missing hover, focus and disabled states, because nothing forced you to define them
  • Assuming the DOM is stable. Headless libraries can change their rendered structure between versions; CSS written against their internal structure is as brittle as any other structural selector — Design System Versioning