frontend / component libraries / headless_ui_and_radix.md

Headless UI and Radix

3 min read source

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

  • asChild requires a single, ref-forwarding child — multiple children or a child that drops ref/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 escape overflow: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/Slot merges behavior onto my element instead of adding wrappers — an alternative to a polymorphic as.”
  • “They support controlled and uncontrolled, and expose data-state so 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

Further reading