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
| Headless | Fully styled library | |
|---|---|---|
| Visual control | Total | Fight the defaults |
| Time to first screen | Slower | Fast |
| Accessibility | Handled | Handled |
| Bundle | Smaller | Larger |
| Upgrade risk | Lower — no CSS to break | Higher |
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.
Related patterns
- 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