Headless UI and Radix
TL;DR
Headless (unstyled) component libraries give you behavior, state, keyboard interaction, and accessibility — and zero styling. You bring the look (Tailwind, CSS, whatever); they bring the hard parts. Radix Primitives, React Aria (Adobe), Headless UI (Tailwind Labs), and Ark UI are the main options. The senior framing: the behavior of widgets like comboboxes and dialogs is genuinely hard and easy to get wrong (see ../16_accessibility/06_accessible_components.md), so separate behavior (library) from presentation (you).
Interview Q&A
Q: What is a “headless” component?
A: A component that implements logic and accessibility but renders no styles (and often minimal/neutral markup). It exposes state and props so you control the DOM/CSS. Contrast with “styled” libraries (MUI, Ant, Chakra) that ship opinionated visuals. Headless = maximum design control + correct a11y; you’re not fighting someone else’s CSS.
Q: Why use one instead of hand-rolling?
A: Because correct widget behavior is a long tail of detail: focus trapping/restoration, roving tabindex, aria-activedescendant, type-ahead, collision-aware positioning, Escape handling, screen-reader announcements, RTL, touch. A vetted primitive has solved and regression-tested all of it. You hand-roll a toggle; you do not hand-roll a production combobox.
Q: What’s Radix’s asChild pattern?
A: Instead of rendering its own element, a Radix primitive can merge its props/behavior onto your child via asChild (powered by a Slot component). This avoids wrapper-element bloat and lets you use your own component/element:
import * as Dialog from "@radix-ui/react-dialog";
<Dialog.Trigger asChild>
<MyButton>Open</MyButton> {/* Radix merges trigger props onto MyButton instead of rendering its own <button> */}
</Dialog.Trigger>
It’s an alternative to the as prop (polymorphic_components.md) — composition by merging onto a single child rather than a polymorphic prop.
Q: Controlled vs uncontrolled in these libraries?
A: Good primitives support both: uncontrolled with defaultValue/defaultOpen (the component owns state) or controlled with value/open + onValueChange/onOpenChange (you own it). Same controlled/uncontrolled model as native inputs (../05_react/controlled_vs_uncontrolled.md) — use uncontrolled until you need to drive/observe the state.
Q: Radix vs Headless UI vs React Aria?
A:
| Radix Primitives | Headless UI | React Aria (Adobe) | |
|---|---|---|---|
| Style | components (<Dialog.Root>…) |
components | hooks (useButton, useComboBox) |
| Coverage | broad, composable | smaller, Tailwind-aligned | broadest behavior + i18n/RTL |
| Ergonomics | compositional parts | simple | lower-level, most flexible |
| Picked for | most React design systems | quick Tailwind apps | deep custom control, i18n |
shadcn/ui is not a dependency — it’s Radix + Tailwind recipes you copy into your repo and own.
Q: How do you style a headless component?
A: Target its parts with classes (Tailwind or CSS), and style based on the data attributes it exposes for state:
<Dialog.Content className="rounded-lg bg-white p-6 data-[state=open]:animate-in" />
Radix sets data-state, data-disabled, data-side, etc., so you style states in CSS without tracking them yourself — no className={isOpen ? ...} plumbing.
Gotchas / edge cases
asChildrequires a single, ref-forwarding child — multiple children or a child that dropsref/props breaks the merge.- Headless ≠ automatically accessible if you misuse it — e.g., not labeling a dialog, or breaking the part structure. You still must wire labels and follow the composition.
- Positioning/portals — popovers/tooltips render in a portal (
Dialog.Portal) to escapeoverflow:hidden/stacking contexts; remember to theme the portal root and handle z-index (../02_css/). - Animation on unmount — content leaving needs the library’s exit-animation hook (
forceMount+ presence) or it disappears before the transition. - Bundle: import only the primitives you use; tree-shaking keeps it lean, but pulling a whole kit you barely use isn’t free.
What a senior is expected to say
- “Headless libraries give behavior + a11y, I bring styles. I don’t hand-roll a combobox/dialog — Radix/React Aria solved the focus/keyboard/ARIA long tail.”
- “Radix’s
asChild/Slotmerges behavior onto my element instead of adding wrappers — an alternative to a polymorphicas.” - “They support controlled and uncontrolled, and expose
data-stateso I style states in CSS.” - “React Aria is hook-level and best for deep custom control + i18n; Radix is the common design-system choice; shadcn is copy-in recipes over Radix.”
Cross-references
- The accessibility these libraries implement: ../16_accessibility/06_accessible_components.md
- Polymorphic
as(the other composition approach): polymorphic_components.md - Controlled vs uncontrolled: ../05_react/controlled_vs_uncontrolled.md
- Styling them with Tailwind: Tailwind/utility_classes.md
Further reading
- Radix Primitives: https://www.radix-ui.com/primitives/docs/overview/introduction
- React Aria: https://react-spectrum.adobe.com/react-aria/
- Headless UI: https://headlessui.com/