Why Every ReactZero Library Ships Two APIs
Headless hooks and styled components from the same package. The dual-API decision behind @reactzero, what it costs to maintain, and the one rule that keeps it honest.
Every component library eventually reaches a fork in the road, and most pick a lane. You can ship styled components — drop in <Combo>, it looks good in five minutes, and it fights you the day your design diverges from the library's. Or you can ship headless hooks — total control, survives any redesign, and costs a beginner an afternoon to render their first dropdown.
The ReactZero libraries ship both, from every package. I want to be honest about why that's a real decision with a real cost, not a free "best of both worlds."
What "Two APIs" Actually Looks Like
Take the combobox. The headless layer is a hook:
const {
isOpen, filteredItems, highlightedIndex,
getLabelProps, getInputProps, getMenuProps, getItemProps,
} = useCombo({ items });
You own every element; the hook hands you prop getters that spread the correct ARIA — role, aria-expanded, aria-activedescendant — onto whatever markup you write. Full control, and you physically can't forget the accessibility because it rides inside the getters.
The styled layer is a component: <Combo>, drop-in, themed by 40-plus CSS custom properties, working in one line.
The data grid is built the same way. The headless useGrid returns prop getters:
const { getGridProps, getRowProps, getCellProps } = useGrid({ data, columns });
// getGridProps() → { role: 'grid' }
// getRowProps(row, i) → { role: 'row', ... }
…and the <Grid> component is what you reach for when you don't want to wire that up yourself. The datepicker follows suit: seven components sitting on four headless hooks.
Two doors into the same room. One for "make it work now," one for "make it exactly mine."
The Cost I Don't Want to Hide
Here's the part the "best of both worlds" pitch skips: every feature is designed twice.
A new capability — say, a "clear selection" affordance on the combobox — has to be expressed to two audiences. The headless user needs a getClearButtonProps() getter and the state to drive it. The styled user needs a clearable prop and a rendered button. Same capability, two surfaces, and both have to stay in sync forever. That's real maintenance tax. Anyone who tells you a dual API is free is either not maintaining one or not maintaining it well.
So the question isn't "isn't two APIs nicer?" It's "is the tax worth it?" For a library meant to be adopted — by teams with their own design systems and by teams who just need a working control today — I think it is, because the alternative is losing half your users at the door. But it's a bet you should make on purpose.
The One Rule That Keeps It Honest
A dual API rots the instant the two sides drift — when the styled component can do something the hook can't, or vice versa. The rule that prevents it is simple and absolute:
The styled layer must be implemented on the headless layer. No private channel between them.
<Combo> is built out of useCombo. <Grid> is built out of useGrid. The component has no backstage access to internal state the hook doesn't expose. If the styled component needs something, the hook has to expose it publicly first — which means every headless user gets it too.
This turns the styled component into a permanent integration test for the headless API. If I can't build the component cleanly from the hooks, the hooks are incomplete, and I find out immediately — not in a bug report from someone six months into a custom build. The component's existence continuously proves the hook is capable enough to build a real, accessible, themed UI. That's worth more than any unit test I could write for the hook in isolation.
It also disciplines the API surface. When you're forced to build the "easy" version out of the "powerful" version, you can't paper over a missing primitive with a shortcut — you have to add the primitive. The headless API gets better because the styled one has to exist.
When I'd Ship Headless-Only
The honest counterweight: this isn't the right call for everything. If a component's whole value is control — a primitive so low-level that anyone using it already wants to own the markup — the styled layer is dead weight, a second surface to maintain for users who'll never touch it. The dual API earns its tax when there's a genuine "just make it work" audience and a "make it mine" audience for the same component. A combobox has both. A raw focus-trap primitive mostly has one.
The pattern generalizes past my libraries. Any time you offer convenience and control, build the convenience out of the control, never beside it. The styled component that's secretly reaching around its own hook is a design-system bug waiting to happen — the two APIs will disagree, and your users will find the seam before you do.
The full architecture of all four libraries — combo, datepicker, flow, and lattice — including the dead ends I kept, is in the case study. Related: the state machine hiding inside your components and the prop I regret adding.