HeadlessUI React
- Overview
HeadlessUI provides completely unstyled, fully accessible UI components for React. Components handle all the complex accessibility and interaction logic — you provide all the styling. 38 playground examples are available via MCP.
All HeadlessUI React examples are available through the frontend-components MCP server under the headlessui-react framework.
- Installation
npm install @headlessui/react
Optional (for icons):
npm install @heroicons/react
- MCP Workflow
3.1 Browse Available Examples
list_components(framework: "headlessui-react")
Components: combobox, dialog, disclosure, listbox, menu, popover, radio-group, switch, tabs, transitions, combinations, suspense.
3.2 Get Example Code
get_component(framework: "headlessui-react", category: "components", component_type: "dialog", variant: "dialog") get_component(framework: "headlessui-react", category: "components", component_type: "menu", variant: "menu") get_component(framework: "headlessui-react", category: "components", component_type: "combobox", variant: "combobox")
3.3 Search
search_components(query: "listbox", framework: "headlessui-react") search_components(query: "transition", framework: "headlessui-react")
- Core API Patterns
4.1 Compound Components
HeadlessUI uses compound component patterns. Parent manages state, children render UI:
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
function MyMenu() { return ( <Menu> <MenuButton className="...">Options</MenuButton> <MenuItems className="..."> <MenuItem> <a className="..." href="/edit">Edit</a> </MenuItem> <MenuItem> <a className="..." href="/delete">Delete</a> </MenuItem> </MenuItems> </Menu> ); }
4.2 The as Prop
Change the rendered element for any component:
<MenuButton as="div" className="...">Options</MenuButton> <MenuItem as="button" className="...">Edit</MenuItem> <DialogPanel as="form" className="...">...</DialogPanel>
4.3 Render Props / Data Attributes
HeadlessUI exposes component state through data attributes for styling:
<MenuItem> <a className="data-[focus]:bg-blue-100 data-[disabled]:opacity-50" href="/edit"
Edit
</a> </MenuItem>
<Switch checked={enabled} onChange={setEnabled} className="data-[checked]:bg-blue-600 bg-gray-200" />
Key data attributes:
Attribute Components Meaning
data-[focus]
MenuItem, ListboxOption, ComboboxOption Item has focus
data-[active]
MenuItem, ListboxOption Item is active
data-[selected]
ListboxOption, ComboboxOption, Tab Item is selected
data-[checked]
Switch, RadioGroupOption Item is checked
data-[open]
Disclosure, Popover, Menu, Dialog Panel is open
data-[disabled]
Most components Item is disabled
data-[closed]
Transition targets Element is closed/hidden
- Component Reference
5.1 Dialog (Modal)
import { Dialog, DialogPanel, DialogTitle, DialogBackdrop } from "@headlessui/react";
function MyDialog({ isOpen, onClose }) { return ( <Dialog open={isOpen} onClose={onClose} className="relative z-50"> <DialogBackdrop className="fixed inset-0 bg-black/30" /> <div className="fixed inset-0 flex w-screen items-center justify-center p-4"> <DialogPanel className="max-w-lg rounded bg-white p-6 shadow-xl"> <DialogTitle className="text-lg font-bold">Title</DialogTitle> <p>Content here</p> <button onClick={onClose}>Close</button> </DialogPanel> </div> </Dialog> ); }
Key features:
-
Focus trapping — focus stays within dialog
-
Scroll locking — page scroll disabled when open
-
Click outside to close (default)
-
Escape key to close (default)
5.2 Menu (Dropdown)
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
<Menu> <MenuButton className="rounded bg-gray-100 px-3 py-2">Options</MenuButton> <MenuItems anchor="bottom start" className="rounded bg-white shadow-lg ring-1 ring-black/5"
<MenuItem>
<button className="block w-full px-4 py-2 text-left data-[focus]:bg-gray-100">
Edit
</button>
</MenuItem>
<MenuItem>
<button className="block w-full px-4 py-2 text-left data-[focus]:bg-gray-100">
Delete
</button>
</MenuItem>
</MenuItems> </Menu>
5.3 Listbox (Select)
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from "@headlessui/react";
function MySelect({ value, onChange, options }) { return ( <Listbox value={value} onChange={onChange}> <ListboxButton className="...">{value.name}</ListboxButton> <ListboxOptions anchor="bottom" className="..."> {options.map((option) => ( <ListboxOption key={option.id} value={option} className="data-[focus]:bg-blue-100 cursor-pointer px-4 py-2" > {option.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ); }
5.4 Combobox (Autocomplete)
import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption, ComboboxButton } from "@headlessui/react";
function MyCombobox({ value, onChange, options }) { const [query, setQuery] = useState(""); const filtered = query === "" ? options : options.filter((o) => o.name.toLowerCase().includes(query.toLowerCase()));
return ( <Combobox value={value} onChange={onChange} onClose={() => setQuery("")}> <div className="relative"> <ComboboxInput className="..." displayValue={(o) => o?.name} onChange={(e) => setQuery(e.target.value)} /> <ComboboxButton className="absolute inset-y-0 right-0 px-2.5">▼</ComboboxButton> </div> <ComboboxOptions anchor="bottom" className="..."> {filtered.map((option) => ( <ComboboxOption key={option.id} value={option} className="data-[focus]:bg-blue-100 px-4 py-2"> {option.name} </ComboboxOption> ))} </ComboboxOptions> </Combobox> ); }
5.5 Switch (Toggle)
import { Switch } from "@headlessui/react";
<Switch checked={enabled} onChange={setEnabled} className="group relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full bg-gray-200 transition-colors data-[checked]:bg-blue-600"
<span className="pointer-events-none inline-block size-5 translate-x-0.5 rounded-full bg-white shadow ring-0 transition-transform group-data-[checked]:translate-x-5" /> </Switch>
5.6 Disclosure (Accordion)
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/react";
<Disclosure> <DisclosureButton className="flex w-full justify-between rounded-lg bg-gray-100 px-4 py-2"> Section Title </DisclosureButton> <DisclosurePanel className="px-4 py-2 text-gray-500"> Content here </DisclosurePanel> </Disclosure>
5.7 Popover
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
<Popover className="relative"> <PopoverButton className="...">Info</PopoverButton> <PopoverPanel anchor="bottom" className="..."> Popover content </PopoverPanel> </Popover>
5.8 RadioGroup
import { RadioGroup, Radio, Label, Description } from "@headlessui/react";
<RadioGroup value={selected} onChange={setSelected}> {options.map((option) => ( <Radio key={option.id} value={option} className="group relative flex cursor-pointer rounded-lg border px-5 py-4 data-[checked]:bg-blue-50 data-[checked]:border-blue-500" > <Label className="font-medium">{option.name}</Label> <Description className="text-gray-500">{option.desc}</Description> </Radio> ))} </RadioGroup>
5.9 Tabs
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@headlessui/react";
<TabGroup> <TabList className="flex gap-4"> <Tab className="rounded-full px-3 py-1 data-[selected]:bg-blue-100 data-[selected]:text-blue-700"> Tab 1 </Tab> <Tab className="rounded-full px-3 py-1 data-[selected]:bg-blue-100 data-[selected]:text-blue-700"> Tab 2 </Tab> </TabList> <TabPanels className="mt-3"> <TabPanel>Panel 1</TabPanel> <TabPanel>Panel 2</TabPanel> </TabPanels> </TabGroup>
- Transitions
HeadlessUI includes a built-in Transition component. Use Tailwind classes for animations:
import { Transition } from "@headlessui/react";
<Transition show={isOpen} enter="transition-opacity duration-150" enterFrom="opacity-0" enterTo="opacity-100" leave="transition-opacity duration-150" leaveFrom="opacity-100" leaveTo="opacity-0"
<div>Animated content</div> </Transition>
Many components support built-in transition via transition prop:
<MenuItems transition className="transition duration-100 ease-in data-[closed]:scale-95 data-[closed]:opacity-0"
- Floating UI (Positioning)
HeadlessUI uses Floating UI internally for positioning dropdown panels. Control with the anchor prop:
<MenuItems anchor="bottom start"> {/* Below, left-aligned /} <MenuItems anchor="bottom end"> {/ Below, right-aligned /} <MenuItems anchor="top start"> {/ Above, left-aligned /} <ListboxOptions anchor="bottom"> {/ Below, centered */}
Anchor values: top , right , bottom , left with optional start /end alignment.
Add gap with anchor={{ to: 'bottom', gap: 4 }} .
- Accessibility Features
HeadlessUI handles these automatically:
-
ARIA attributes (role , aria-expanded , aria-haspopup , aria-labelledby , etc.)
-
Keyboard navigation (arrow keys, Enter, Space, Escape, Home, End)
-
Focus management (trapping in dialogs, return focus on close)
-
Screen reader announcements
-
Click-outside-to-close for menus, popovers, dialogs
- Common Patterns
9.1 Controlled vs Uncontrolled
// Controlled — you manage state const [selected, setSelected] = useState(options[0]); <Listbox value={selected} onChange={setSelected}>
// Uncontrolled — HeadlessUI manages state <Listbox defaultValue={options[0]} onChange={(val) => console.log(val)}>
9.2 Form Integration
HeadlessUI components work with native forms when given a name :
<Listbox name="country" value={selected} onChange={setSelected}> {/* renders a hidden input with the selected value */} </Listbox>
9.3 Disabled Items
<MenuItem disabled> <button className="data-[disabled]:opacity-50" disabled> Can't click </button> </MenuItem>
- Workflow Summary
Step Action
-
Pick component Dialog, Menu, Listbox, Combobox, Switch, etc.
-
Fetch example get_component(framework: "headlessui-react", ...)
-
Import import { Component } from "@headlessui/react"
-
Style with Tailwind Use className + data attributes for states
-
Add transitions Use transition prop or Transition component
-
Position Use anchor prop for dropdowns/popovers