Headless UI Architectures: Decoupling Logic from Presentation
Master Headless UI architectures to build fully decoupled, reusable React components. Learn to separate state from markup for ultimate design flexibility.
Previously in this course, we explored designing compound components to create expressive APIs. While compound components solve the "API ergonomics" problem, they often bake UI decisions—like structure and styling—directly into the component tree.
Today, we take that a step further with Headless UI. Instead of providing a component that is a button or a menu, we provide a hook that manages the logic of a button or menu, leaving the rendering entirely to you.
What is Headless UI?
A "headless" component is a piece of logic that handles state, accessibility (WAI-ARIA), and event orchestration without rendering any visual elements (HTML) of its own. By extracting this logic, you achieve maximum reusability and design system flexibility.
Think of it as the engine of a car. You can put that engine into a sedan, a sports car, or a truck—the mechanics remain the same, but the "skin" changes entirely.
Building a Headless Primitive
To build a headless primitive, we move away from return <div>...</div> and toward a hook-based API that returns state and event handlers. This approach is similar to how we approached extracting custom hooks in earlier lessons, but focused specifically on UI interaction patterns.
Let's build a simple useToggle headless primitive, which is the foundational building block for dropdowns, modals, and accordions.
The Implementation
JAVASCRIPTimport { useState, useCallback } from CE9178">'react'; // The "Headless" Hook: Pure logic, zero UI export function useToggle(initialState = false) { const [isOpen, setIsOpen] = useState(initialState); const toggle = useCallback(() => setIsOpen((prev) => !prev), []); const open = useCallback(() => setIsOpen(true), []); const close = useCallback(() => setIsOpen(false), []); return { isOpen, toggle, open, close, // Prop getters for accessibility getButtonProps: () => ({ CE9178">'aria-expanded': isOpen, onClick: toggle, }), }; }
Decoupling Logic from Markup
Now that we have the logic, we can apply it to any UI we want. Because the hook returns a getButtonProps helper, we ensure that our implementation remains accessible without the consumer having to remember the boilerplate aria attributes.
JSX// Usage in a component function MyDropdown() { const { isOpen, getButtonProps } = useToggle(); return ( <div> <button {...getButtonProps()}>Toggle Menu</button> {isOpen && <ul><li>Option 1</li></ul>} </div> ); }
By using this pattern, you can swap the button for a custom styled div, an icon, or a menu trigger without ever touching the logic. This is the essence of building a scalable design system.
Hands-on Exercise
Your task is to extend the useToggle hook to include a "click outside to close" feature.
- Create a
useClickOutsidehook that accepts arefand ahandler. - Integrate this into a new headless
useDropdownhook that combinesuseToggleanduseClickOutside. - Use this to create a dropdown component that closes automatically when the user clicks anywhere else on the page.
Common Pitfalls
- Over-abstraction: Don't turn every UI element into a headless hook. If a component is simple and unlikely to be restyled, keep it simple. Only use Headless UI when you need to support multiple visual implementations of the same interaction.
- Prop Getter Complexity: Avoid making
getButtonPropstoo complex. If it starts accepting too many arguments, you're likely coupling the hook too tightly to the UI. - Ignoring Accessibility: The primary benefit of a headless library is handling the "hard" accessibility parts (like keyboard navigation). If you build a headless component, ensure your props include
onKeyDownhandlers forEscape,Enter, and arrow keys.
Recap
- Headless UI separates state and interaction logic from visual rendering.
- Hooks are the ideal vehicle for this separation, as they allow us to compose logic without creating unnecessary DOM nodes.
- Design Systems benefit significantly from this, as you can maintain one "engine" for complex interactions (like comboboxes or menus) while keeping the visual layer completely flexible.
By moving the heavy lifting into headless primitives, you ensure that your UI components remain thin, testable, and highly adaptable to changing design requirements.
Up next: We'll look at Modular Directory Structures to organize these headless primitives alongside our feature-based components.
Work with me

Headless WordPress + Next.js Frontend Development
Keep WordPress for content, get a lightning-fast Next.js frontend. The best of both worlds — familiar editing, modern speed.

Next.js Website & Landing Page Development
A blazing-fast, SEO-optimized website or landing page in Next.js — the kind that loads instantly and ranks. Design-to-code, done right.