Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Theme
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Create API Key
Add Docs
Headless UI
https://github.com/tailwindlabs/headlessui
Admin
Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind
...
Tokens:
14,870
Snippets:
48
Trust Score:
8
Update:
4 months ago
Context
Skills
Chat
Benchmark
59.5
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Headless UI Headless UI is a collection of completely unstyled, fully accessible UI components for React and Vue, designed to integrate seamlessly with Tailwind CSS. The library provides essential interactive components like menus, dialogs, listboxes, comboboxes, and more, implementing WAI-ARIA design patterns for accessibility. Components are "headless" meaning they provide functionality and accessibility without imposing any styling, giving developers complete control over the visual presentation while handling complex state management, keyboard navigation, focus management, and screen reader support automatically. The library is distributed as three npm packages: `@headlessui/react` for React applications (currently v2.2.9), `@headlessui/vue` for Vue 3 applications, and `@headlessui/tailwindcss` as a Tailwind CSS plugin that provides utility variants for styling components based on their internal state. All components support both controlled and uncontrolled modes, work seamlessly with form submissions, and offer flexible APIs through render props, allowing developers to access internal component state for conditional rendering and dynamic styling. The components handle edge cases like scroll locking, focus trapping, click-outside detection, and proper cleanup automatically. ## Installation ### React Installation ```bash npm install @headlessui/react ``` ```jsx import { Menu, Dialog, Listbox } from '@headlessui/react' function App() { return ( <Menu> <Menu.Button>Options</Menu.Button> <Menu.Items> <Menu.Item>{({ active }) => <a className={active ? 'bg-blue-500' : ''}>Account</a>}</Menu.Item> </Menu.Items> </Menu> ) } ``` ### Vue Installation ```bash npm install @headlessui/vue ``` ```vue <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script> <template> <Menu> <MenuButton>Options</MenuButton> <MenuItems> <MenuItem v-slot="{ active }"> <a :class="active ? 'bg-blue-500' : ''">Account</a> </MenuItem> </MenuItems> </Menu> </template> ``` ### Tailwind CSS Plugin Installation ```bash npm install @headlessui/tailwindcss ``` ```js // tailwind.config.js module.exports = { plugins: [ require('@headlessui/tailwindcss'), // Or with custom prefix require('@headlessui/tailwindcss')({ prefix: 'ui' }) ] } ``` ## Button Component Accessible button with state management for hover, focus, active, and disabled states ```jsx import { Button } from '@headlessui/react' function ActionButton() { return ( <Button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50" onClick={() => console.log('Clicked!')} > Click me </Button> ) } // With render prop for state-based styling function StatefulButton() { return ( <Button disabled> {({ disabled, hover, focus, active }) => ( <span className={` px-4 py-2 rounded ${disabled ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-500'} ${hover && !disabled ? 'bg-blue-600' : ''} ${focus ? 'ring-2 ring-blue-400' : ''} ${active ? 'scale-95' : ''} `}> {disabled ? 'Disabled' : 'Active'} </span> )} </Button> ) } // Button types function FormButtons() { return ( <form> <Button type="submit" className="px-4 py-2 bg-green-500 text-white rounded"> Submit </Button> <Button type="reset" className="px-4 py-2 bg-gray-500 text-white rounded"> Reset </Button> <Button type="button" className="px-4 py-2 bg-blue-500 text-white rounded"> Action </Button> </form> ) } ``` ## Checkbox Component Accessible checkbox with indeterminate state support and form integration ```jsx import { Checkbox, Field, Label, Description } from '@headlessui/react' import { useState } from 'react' function TermsCheckbox() { const [enabled, setEnabled] = useState(false) return ( <Field className="flex items-center gap-2"> <Checkbox checked={enabled} onChange={setEnabled} className="group size-6 rounded-md bg-white/10 p-1 ring-1 ring-white/15 ring-inset data-[checked]:bg-blue-500" > <svg className="hidden size-4 fill-white group-data-[checked]:block" viewBox="0 0 14 14"> <path d="M3 8L6 11L11 3.5" stroke="currentColor" strokeWidth={2} fill="none" /> </svg> </Checkbox> <Label>I agree to the terms and conditions</Label> </Field> ) } // Indeterminate checkbox function SelectAllCheckbox() { const [selectedItems, setSelectedItems] = useState([]) const items = ['Item 1', 'Item 2', 'Item 3'] const allSelected = selectedItems.length === items.length const someSelected = selectedItems.length > 0 && !allSelected return ( <div> <Checkbox checked={allSelected} indeterminate={someSelected} onChange={(checked) => setSelectedItems(checked ? items : [])} className="size-6 rounded border border-gray-300" > {({ checked, indeterminate }) => ( <> {indeterminate ? '−' : checked ? '✓' : ''} </> )} </Checkbox> {items.map((item) => ( <Checkbox key={item} checked={selectedItems.includes(item)} onChange={(checked) => { setSelectedItems(checked ? [...selectedItems, item] : selectedItems.filter(i => i !== item) ) }} > {item} </Checkbox> ))} </div> ) } // Uncontrolled checkbox with form integration function NewsletterForm() { return ( <form onSubmit={(e) => { e.preventDefault() const formData = new FormData(e.currentTarget) console.log('Subscribe:', formData.get('newsletter')) // Output: "on" if checked, null if unchecked }}> <Checkbox name="newsletter" defaultChecked className="size-5 rounded"> {({ checked }) => ( <span className={checked ? 'bg-blue-500' : 'bg-gray-200'}> {checked && '✓'} </span> )} </Checkbox> <button type="submit">Subscribe</button> </form> ) } ``` ## Field, Fieldset, Label, Description, and Legend Components Form layout components for accessible form field grouping and labeling ```jsx import { Field, Fieldset, Label, Description, Legend, Input, Checkbox } from '@headlessui/react' function UserProfileForm() { return ( <form className="space-y-6"> {/* Single field with label and description */} <Field> <Label className="block text-sm font-medium text-gray-700">Email address</Label> <Description className="text-sm text-gray-500"> We'll never share your email with anyone else. </Description> <Input type="email" name="email" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm" /> </Field> {/* Fieldset for grouping related fields */} <Fieldset className="space-y-4 rounded-lg border border-gray-200 p-4"> <Legend className="text-lg font-semibold">Notification Preferences</Legend> <Field className="flex items-center gap-2"> <Checkbox name="email-notifications" /> <Label>Email notifications</Label> </Field> <Field className="flex items-center gap-2"> <Checkbox name="sms-notifications" /> <Label>SMS notifications</Label> <Description className="text-sm text-gray-500"> Standard message rates may apply </Description> </Field> </Fieldset> {/* Disabled fieldset */} <Fieldset disabled className="opacity-50"> <Legend>Premium Features (Upgrade Required)</Legend> <Field> <Label>API Access</Label> <Input type="text" placeholder="API Key" /> </Field> </Fieldset> </form> ) } // Field with render prop for state access function DynamicField() { return ( <Field disabled> {({ disabled }) => ( <> <Label className={disabled ? 'text-gray-400' : 'text-gray-900'}> Username </Label> <Input type="text" className={disabled ? 'bg-gray-100' : 'bg-white'} /> </> )} </Field> ) } ``` ## Input Component Accessible text input with automatic label and description association ```jsx import { Input, Field, Label, Description } from '@headlessui/react' import { useState } from 'react' function EmailInput() { const [email, setEmail] = useState('') return ( <Field> <Label>Email address</Label> <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="block w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" placeholder="you@example.com" /> </Field> ) } // Input with validation state function ValidatedInput() { const [value, setValue] = useState('') const isInvalid = value.length > 0 && value.length < 3 return ( <Field> <Label>Username (min 3 characters)</Label> <Input value={value} onChange={(e) => setValue(e.target.value)} invalid={isInvalid} className="data-[invalid]:border-red-500 data-[focus]:ring-2" /> {isInvalid && ( <Description className="text-red-500 text-sm"> Username must be at least 3 characters </Description> )} </Field> ) } // Input with render prop for state-based styling function StateBasedInput() { return ( <Input autoFocus> {({ focus, hover, disabled, invalid }) => ( <input className={` w-full rounded border px-3 py-2 ${focus ? 'ring-2 ring-blue-500 border-blue-500' : 'border-gray-300'} ${hover && !focus ? 'border-gray-400' : ''} ${disabled ? 'bg-gray-100 cursor-not-allowed' : ''} ${invalid ? 'border-red-500' : ''} `} /> )} </Input> ) } ``` ## Select Component Native select element with accessibility features and state management ```jsx import { Select, Field, Label } from '@headlessui/react' import { useState } from 'react' function CountrySelect() { const [country, setCountry] = useState('us') return ( <Field> <Label>Country</Label> <Select value={country} onChange={(e) => setCountry(e.target.value)} className="block w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" > <option value="us">United States</option> <option value="ca">Canada</option> <option value="mx">Mexico</option> <option value="uk">United Kingdom</option> </Select> </Field> ) } // Select with render prop function StyledSelect() { return ( <Select> {({ focus, hover, active, disabled }) => ( <select className={` rounded border px-3 py-2 ${focus ? 'ring-2 ring-blue-500' : ''} ${hover ? 'border-gray-400' : 'border-gray-300'} ${disabled ? 'bg-gray-100' : ''} `} > <option>Option 1</option> <option>Option 2</option> </select> )} </Select> ) } // Invalid state handling function FormSelect() { const [value, setValue] = useState('') const isInvalid = value === '' return ( <Field> <Label>Required Selection</Label> <Select value={value} onChange={(e) => setValue(e.target.value)} invalid={isInvalid} className="data-[invalid]:border-red-500" > <option value="">Select an option...</option> <option value="1">Option 1</option> <option value="2">Option 2</option> </Select> </Field> ) } ``` ## Textarea Component Multi-line text input with automatic resizing and accessibility ```jsx import { Textarea, Field, Label, Description } from '@headlessui/react' import { useState } from 'react' function CommentTextarea() { const [comment, setComment] = useState('') return ( <Field> <Label>Leave a comment</Label> <Description>Share your thoughts with the community</Description> <Textarea value={comment} onChange={(e) => setComment(e.target.value)} rows={4} className="block w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" placeholder="Write your comment here..." /> <Description className="text-sm text-gray-500"> {comment.length} / 500 characters </Description> </Field> ) } // Textarea with validation function BioTextarea() { const [bio, setBio] = useState('') const isInvalid = bio.length > 200 return ( <Field> <Label>Bio</Label> <Textarea value={bio} onChange={(e) => setBio(e.target.value)} invalid={isInvalid} className="w-full rounded border px-3 py-2 data-[invalid]:border-red-500" /> {isInvalid && ( <Description className="text-red-500 text-sm"> Bio must be 200 characters or less </Description> )} </Field> ) } // Textarea with render prop function StateBasedTextarea() { return ( <Textarea> {({ focus, hover, disabled, invalid }) => ( <textarea rows={3} className={` w-full rounded border px-3 py-2 ${focus ? 'ring-2 ring-blue-500 border-blue-500' : 'border-gray-300'} ${hover && !focus ? 'border-gray-400' : ''} ${disabled ? 'bg-gray-100 cursor-not-allowed' : ''} ${invalid ? 'border-red-500' : ''} `} /> )} </Textarea> ) } ``` ## DataInteractive Component Utility component for tracking interactive states (hover, focus, active) on any element ```jsx import { DataInteractive } from '@headlessui/react' function InteractiveCard() { return ( <DataInteractive> {({ hover, focus, active }) => ( <div className={` p-6 rounded-lg border-2 transition-all ${hover ? 'border-blue-500 shadow-lg' : 'border-gray-300'} ${focus ? 'ring-2 ring-blue-400' : ''} ${active ? 'scale-95' : 'scale-100'} `} tabIndex={0} > <h3 className="text-lg font-bold">Interactive Card</h3> <p className="text-gray-600"> Hover, focus, or click to see state changes </p> <div className="mt-2 text-sm"> <span>Hover: {hover ? '✓' : '✗'}</span> |{' '} <span>Focus: {focus ? '✓' : '✗'}</span> |{' '} <span>Active: {active ? '✓' : '✗'}</span> </div> </div> )} </DataInteractive> ) } // Custom interactive button with state indicators function StateAwareButton() { return ( <DataInteractive> {({ hover, focus, active }) => ( <button className={` px-6 py-3 rounded-md font-medium transition-all ${active ? 'bg-blue-700' : hover ? 'bg-blue-600' : 'bg-blue-500'} ${focus ? 'ring-4 ring-blue-300' : ''} text-white `} onClick={() => console.log('Button clicked!')} > {active ? 'Clicking...' : hover ? 'Click me!' : 'Hover me'} </button> )} </DataInteractive> ) } // Interactive list item with custom styling function InteractiveListItem({ title, description }) { return ( <DataInteractive> {({ hover, focus, active }) => ( <div className={` p-4 rounded cursor-pointer ${hover ? 'bg-gray-100' : 'bg-white'} ${focus ? 'outline outline-2 outline-blue-500' : ''} ${active ? 'bg-gray-200' : ''} `} tabIndex={0} role="button" > <h4 className="font-semibold">{title}</h4> <p className="text-sm text-gray-600">{description}</p> </div> )} </DataInteractive> ) } ``` ## CloseButton Component Utility component for closing parent Dialog, Popover, or other closable components ```jsx import { Dialog, CloseButton } from '@headlessui/react' import { useState } from 'react' function DialogWithCloseButton() { const [isOpen, setIsOpen] = useState(false) return ( <> <button onClick={() => setIsOpen(true)}>Open Dialog</button> <Dialog open={isOpen} onClose={() => setIsOpen(false)}> <Dialog.Panel> <div className="flex items-center justify-between mb-4"> <Dialog.Title>Settings</Dialog.Title> <CloseButton className="p-2 hover:bg-gray-100 rounded"> ✕ </CloseButton> </div> <Dialog.Description> Manage your account settings and preferences. </Dialog.Description> <div className="mt-4 flex gap-2"> <CloseButton className="px-4 py-2 bg-gray-200 rounded"> Cancel </CloseButton> <button className="px-4 py-2 bg-blue-500 text-white rounded"> Save </button> </div> </Dialog.Panel> </Dialog> </> ) } // CloseButton in Popover function PopoverWithClose() { return ( <Popover> <Popover.Button>Open</Popover.Button> <Popover.Panel className="absolute bg-white shadow-lg rounded-lg p-4"> <div className="flex justify-between items-start"> <h3>Notification</h3> <CloseButton className="text-gray-400 hover:text-gray-600"> ✕ </CloseButton> </div> <p className="mt-2">You have new messages</p> </Popover.Panel> </Popover> ) } // Using useClose hook for programmatic closing import { useClose } from '@headlessui/react' function CustomCloseButton() { let close = useClose() return ( <button onClick={() => { // Perform some action before closing console.log('Saving data...') close() }} className="px-4 py-2 bg-blue-500 text-white rounded" > Save and Close </button> ) } ``` ## Menu Component Dropdown menu with keyboard navigation, focus management, and ARIA attributes ```jsx import { Menu } from '@headlessui/react' function MyMenu() { return ( <Menu> {({ open, close }) => ( <> <Menu.Button className="px-4 py-2 bg-blue-500 text-white rounded"> Actions {open ? '▲' : '▼'} </Menu.Button> <Menu.Items className="absolute mt-2 bg-white shadow-lg rounded-lg py-1 w-48"> <Menu.Item> {({ active, disabled }) => ( <a href="/account" className={`block px-4 py-2 ${active ? 'bg-blue-500 text-white' : 'text-gray-900'}`} > Account settings </a> )} </Menu.Item> <Menu.Item disabled> <span className="block px-4 py-2 text-gray-400 cursor-not-allowed"> Disabled item </span> </Menu.Item> <Menu.Item> {({ close }) => ( <button onClick={() => { console.log('Logging out'); close(); }} className="block w-full text-left px-4 py-2 text-red-600 hover:bg-red-50" > Sign out </button> )} </Menu.Item> </Menu.Items> </> )} </Menu> ) } ``` ## Dialog Component Modal overlay with focus trap, scroll locking, and backdrop click-to-close ```jsx import { Dialog } from '@headlessui/react' import { useState } from 'react' function DeleteAccountDialog() { const [isOpen, setIsOpen] = useState(false) return ( <> <button onClick={() => setIsOpen(true)} className="px-4 py-2 bg-red-500 text-white rounded" > Delete Account </button> <Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50" > {/* Backdrop */} <Dialog.Backdrop className="fixed inset-0 bg-black/30" /> {/* Full-screen container to center the panel */} <div className="fixed inset-0 flex items-center justify-center p-4"> <Dialog.Panel className="mx-auto max-w-sm bg-white rounded-lg shadow-xl p-6"> <Dialog.Title className="text-lg font-bold text-gray-900"> Delete Account </Dialog.Title> <Dialog.Description className="mt-2 text-sm text-gray-500"> This will permanently delete your account and all associated data. This action cannot be undone. </Dialog.Description> <div className="mt-4 flex gap-3 justify-end"> <button onClick={() => setIsOpen(false)} className="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded" > Cancel </button> <button onClick={() => { console.log('Account deleted'); setIsOpen(false); }} className="px-4 py-2 text-sm bg-red-500 text-white rounded hover:bg-red-600" > Delete </button> </div> </Dialog.Panel> </div> </Dialog> </> ) } ``` ## Listbox Component Accessible select dropdown with keyboard navigation and custom value comparison ```jsx import { Listbox } from '@headlessui/react' import { useState } from 'react' function TeamSelector() { const people = [ { id: 1, name: 'Alice Johnson', role: 'Designer', avatar: 'AJ' }, { id: 2, name: 'Bob Smith', role: 'Developer', avatar: 'BS' }, { id: 3, name: 'Charlie Brown', role: 'Manager', avatar: 'CB' }, ] const [selected, setSelected] = useState(people[1]) return ( <div className="w-72"> <Listbox value={selected} onChange={setSelected} by="id"> {({ open }) => ( <> <Listbox.Label className="block text-sm font-medium text-gray-700 mb-1"> Assign to </Listbox.Label> <div className="relative"> <Listbox.Button className="relative w-full cursor-pointer rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <span className="flex items-center"> <span className="inline-block h-6 w-6 rounded-full bg-blue-500 text-white text-xs flex items-center justify-center"> {selected.avatar} </span> <span className="ml-3 block truncate">{selected.name}</span> </span> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> {open ? '▲' : '▼'} </span> </Listbox.Button> <Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> {people.map((person) => ( <Listbox.Option key={person.id} value={person} className={({ active, selected }) => `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ active ? 'bg-blue-100 text-blue-900' : 'text-gray-900' } ${selected ? 'font-semibold' : 'font-normal'}` } > {({ selected }) => ( <> <div className="flex items-center"> <span className="inline-block h-6 w-6 rounded-full bg-blue-500 text-white text-xs flex items-center justify-center"> {person.avatar} </span> <span className="ml-3"> <span className="block">{person.name}</span> <span className="block text-xs text-gray-500">{person.role}</span> </span> </div> {selected && ( <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-blue-600"> ✓ </span> )} </> )} </Listbox.Option> ))} </Listbox.Options> </div> </> )} </Listbox> </div> ) } // Multiple selection mode function MultiSelectListbox() { const [selectedPeople, setSelectedPeople] = useState([]) return ( <Listbox value={selectedPeople} onChange={setSelectedPeople} multiple> <Listbox.Button> {selectedPeople.length === 0 ? 'Select people' : `${selectedPeople.length} selected`} </Listbox.Button> <Listbox.Options> <Listbox.Option value="alice">Alice</Listbox.Option> <Listbox.Option value="bob">Bob</Listbox.Option> <Listbox.Option value="charlie">Charlie</Listbox.Option> </Listbox.Options> </Listbox> ) } ``` ## Combobox Component Autocomplete input with filtering and keyboard navigation ```jsx import { Combobox } from '@headlessui/react' import { useState } from 'react' function UserSearch() { const people = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com' }, { id: 3, name: 'Charlie Brown', email: 'charlie@example.com' }, { id: 4, name: 'Diana Prince', email: 'diana@example.com' }, ] const [selected, setSelected] = useState(null) const [query, setQuery] = useState('') const filteredPeople = query === '' ? people : people.filter(person => person.name.toLowerCase().includes(query.toLowerCase()) || person.email.toLowerCase().includes(query.toLowerCase()) ) return ( <div className="w-72"> <Combobox value={selected} onChange={setSelected} by="id"> <Combobox.Label className="block text-sm font-medium text-gray-700 mb-1"> Search user </Combobox.Label> <div className="relative"> <Combobox.Input className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" displayValue={(person) => person?.name || ''} onChange={(e) => setQuery(e.target.value)} placeholder="Type to search..." /> <Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2"> <span className="text-gray-400">▼</span> </Combobox.Button> <Combobox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> {filteredPeople.length === 0 && query !== '' ? ( <div className="relative cursor-default select-none py-2 px-4 text-gray-700"> No people found. </div> ) : ( filteredPeople.map((person) => ( <Combobox.Option key={person.id} value={person} className={({ active, selected }) => `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ active ? 'bg-blue-500 text-white' : 'text-gray-900' } ${selected ? 'font-semibold' : 'font-normal'}` } > {({ active, selected }) => ( <> <div> <span className="block truncate">{person.name}</span> <span className={`block text-xs truncate ${active ? 'text-blue-200' : 'text-gray-500'}`}> {person.email} </span> </div> {selected && ( <span className="absolute inset-y-0 left-0 flex items-center pl-3"> ✓ </span> )} </> )} </Combobox.Option> )) )} </Combobox.Options> </div> </Combobox> </div> ) } // Multiple selection with tags function TagCombobox() { const [selectedPeople, setSelectedPeople] = useState([]) const [query, setQuery] = useState('') return ( <Combobox value={selectedPeople} onChange={setSelectedPeople} multiple> <div className="flex flex-wrap gap-2 mb-2"> {selectedPeople.map((person) => ( <span key={person} className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm"> {person} </span> ))} </div> <Combobox.Input onChange={(e) => setQuery(e.target.value)} placeholder="Add more..." /> <Combobox.Options> <Combobox.Option value="alice">Alice</Combobox.Option> <Combobox.Option value="bob">Bob</Combobox.Option> <Combobox.Option value="charlie">Charlie</Combobox.Option> </Combobox.Options> </Combobox> ) } ``` ## Popover Component Floating overlay with positioning and click-outside-to-close behavior ```jsx import { Popover } from '@headlessui/react' function SolutionsPopover() { const solutions = [ { name: 'Analytics', description: 'Get insights into your data', href: '/analytics' }, { name: 'Engagement', description: 'Connect with your audience', href: '/engagement' }, { name: 'Security', description: 'Your data is safe', href: '/security' }, ] return ( <Popover className="relative"> {({ open, close }) => ( <> <Popover.Button className="inline-flex items-center gap-x-1 text-sm font-semibold leading-6 text-gray-900"> Solutions <span className={`transition-transform ${open ? 'rotate-180' : ''}`}>▼</span> </Popover.Button> <Popover.Panel className="absolute left-1/2 z-10 mt-5 flex w-screen max-w-max -translate-x-1/2 px-4"> <div className="w-96 shrink rounded-xl bg-white p-4 text-sm leading-6 shadow-lg ring-1 ring-gray-900/5"> {solutions.map((item) => ( <div key={item.name} className="group relative rounded-lg p-4 hover:bg-gray-50"> <a href={item.href} className="font-semibold text-gray-900"> {item.name} <span className="absolute inset-0" /> </a> <p className="mt-1 text-gray-600">{item.description}</p> </div> ))} <button onClick={close} className="mt-4 w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600" > Close </button> </div> </Popover.Panel> </> )} </Popover> ) } // With backdrop overlay function PopoverWithBackdrop() { return ( <Popover> <Popover.Button className="px-4 py-2 bg-blue-500 text-white rounded"> Open Menu </Popover.Button> <Popover.Backdrop className="fixed inset-0 bg-black/30 z-40" /> <Popover.Panel className="fixed inset-x-0 top-1/2 z-50 mx-auto w-96 -translate-y-1/2 rounded-lg bg-white p-6 shadow-2xl"> <h3 className="text-lg font-semibold">Menu</h3> <p className="mt-2 text-gray-600">Content goes here</p> </Popover.Panel> </Popover> ) } ``` ## Disclosure Component Show/hide component for accordions and expandable sections ```jsx import { Disclosure } from '@headlessui/react' function FAQ() { const faqs = [ { question: 'What is your refund policy?', answer: 'If you\'re unhappy with your purchase for any reason, email us within 90 days and we\'ll refund you in full, no questions asked.', }, { question: 'Do you offer technical support?', answer: 'Yes, we offer 24/7 technical support via email and chat for all customers.', }, { question: 'Do you ship internationally?', answer: 'Yes, we ship all over the world. Shipping costs will apply, and will be added at checkout.', }, ] return ( <div className="w-full max-w-2xl mx-auto space-y-2"> {faqs.map((faq, index) => ( <Disclosure key={index} as="div" className="border border-gray-200 rounded-lg"> {({ open }) => ( <> <Disclosure.Button className="flex w-full justify-between items-center px-4 py-3 text-left text-sm font-medium text-gray-900 hover:bg-gray-50 rounded-lg"> <span>{faq.question}</span> <span className={`ml-6 flex-shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}> ▼ </span> </Disclosure.Button> <Disclosure.Panel className="px-4 pb-3 text-sm text-gray-600"> {faq.answer} </Disclosure.Panel> </> )} </Disclosure> ))} </div> ) } // Single disclosure with default open state function SingleDisclosure() { return ( <Disclosure defaultOpen> {({ open, close }) => ( <> <Disclosure.Button className="flex items-center gap-2"> <span>{open ? '−' : '+'}</span> <span>Advanced Settings</span> </Disclosure.Button> <Disclosure.Panel className="mt-4 p-4 bg-gray-50 rounded"> <p className="text-sm text-gray-700"> Advanced configuration options here. </p> <button onClick={close} className="mt-2 text-sm text-blue-600 hover:text-blue-800" > Collapse </button> </Disclosure.Panel> </> )} </Disclosure> ) } ``` ## Tabs Component Tabbed interface with keyboard navigation and automatic selection management ```jsx import { Tab } from '@headlessui/react' import { useState } from 'react' function ProductTabs() { const categories = { Recent: [ { id: 1, title: 'iPhone 15 Pro', date: '2 hours ago' }, { id: 2, title: 'MacBook Pro M3', date: '5 hours ago' }, ], Popular: [ { id: 3, title: 'AirPods Pro', date: 'Most viewed' }, { id: 4, title: 'Apple Watch Ultra', date: 'Most liked' }, ], Trending: [ { id: 5, title: 'Vision Pro', date: 'Trending #1' }, { id: 6, title: 'iPad Air', date: 'Trending #2' }, ], } return ( <div className="w-full max-w-2xl"> <Tab.Group> <Tab.List className="flex space-x-1 rounded-xl bg-blue-900/20 p-1"> {Object.keys(categories).map((category) => ( <Tab key={category} className={({ selected }) => `w-full rounded-lg py-2.5 text-sm font-medium leading-5 ${selected ? 'bg-white text-blue-700 shadow' : 'text-blue-600 hover:bg-white/[0.12] hover:text-blue-800' }` } > {category} </Tab> ))} </Tab.List> <Tab.Panels className="mt-2"> {Object.values(categories).map((posts, idx) => ( <Tab.Panel key={idx} className="rounded-xl bg-white p-3 ring-1 ring-black ring-opacity-5" > <ul className="space-y-2"> {posts.map((post) => ( <li key={post.id} className="relative rounded-md p-3 hover:bg-gray-50" > <h3 className="text-sm font-medium leading-5">{post.title}</h3> <p className="mt-1 text-xs text-gray-500">{post.date}</p> </li> ))} </ul> </Tab.Panel> ))} </Tab.Panels> </Tab.Group> </div> ) } // Controlled tabs with manual selection function ControlledTabs() { const [selectedIndex, setSelectedIndex] = useState(0) return ( <Tab.Group selectedIndex={selectedIndex} onChange={setSelectedIndex}> <div className="flex items-center justify-between mb-4"> <Tab.List className="flex space-x-2"> <Tab className={({ selected }) => `px-4 py-2 rounded ${selected ? 'bg-blue-500 text-white' : 'bg-gray-200'}` }> Overview </Tab> <Tab className={({ selected }) => `px-4 py-2 rounded ${selected ? 'bg-blue-500 text-white' : 'bg-gray-200'}` }> Details </Tab> </Tab.List> <div className="text-sm text-gray-600"> Tab {selectedIndex + 1} of 2 </div> </div> <Tab.Panels> <Tab.Panel>Overview content</Tab.Panel> <Tab.Panel>Details content</Tab.Panel> </Tab.Panels> </Tab.Group> ) } ``` ## Switch Component Toggle switch with form integration and accessibility ```jsx import { Switch } from '@headlessui/react' import { useState } from 'react' function NotificationSettings() { const [emailEnabled, setEmailEnabled] = useState(true) const [smsEnabled, setSmsEnabled] = useState(false) const [pushEnabled, setPushEnabled] = useState(true) return ( <div className="w-full max-w-md space-y-4"> <Switch.Group as="div" className="flex items-center justify-between"> <span className="flex flex-grow flex-col"> <Switch.Label as="span" className="text-sm font-medium text-gray-900" passive> Email notifications </Switch.Label> <Switch.Description as="span" className="text-sm text-gray-500"> Receive email updates about your account activity </Switch.Description> </span> <Switch checked={emailEnabled} onChange={setEmailEnabled} className={`${emailEnabled ? 'bg-blue-600' : 'bg-gray-200'} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`} > <span className={`${emailEnabled ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`} /> </Switch> </Switch.Group> <Switch.Group as="div" className="flex items-center justify-between"> <span className="flex flex-grow flex-col"> <Switch.Label as="span" className="text-sm font-medium text-gray-900" passive> SMS notifications </Switch.Label> <Switch.Description as="span" className="text-sm text-gray-500"> Get text messages about urgent updates </Switch.Description> </span> <Switch checked={smsEnabled} onChange={setSmsEnabled} className={`${smsEnabled ? 'bg-blue-600' : 'bg-gray-200'} relative inline-flex h-6 w-11 items-center rounded-full`} > <span className={`${smsEnabled ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white`} /> </Switch> </Switch.Group> </div> ) } // Form integration with uncontrolled switch function FormWithSwitch() { return ( <form onSubmit={(e) => { e.preventDefault() const formData = new FormData(e.currentTarget) const data = Object.fromEntries(formData) console.log('Form data:', data) // Output: { notifications: "on" } or {} }}> <Switch.Group> <Switch.Label>Enable notifications</Switch.Label> <Switch name="notifications" defaultChecked /> </Switch.Group> <button type="submit" className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"> Save </button> </form> ) } // Render prop pattern with custom UI function CustomSwitch() { const [enabled, setEnabled] = useState(false) return ( <Switch checked={enabled} onChange={setEnabled} className="group"> {({ checked }) => ( <div className="flex items-center space-x-2"> <div className={`w-12 h-6 rounded-full ${checked ? 'bg-green-500' : 'bg-red-500'} relative`}> <div className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform ${checked ? 'translate-x-6' : ''}`} /> </div> <span className="text-sm font-medium"> {checked ? 'ON' : 'OFF'} </span> </div> )} </Switch> ) } ``` ## RadioGroup Component Accessible radio button group with complex object values ```jsx import { RadioGroup } from '@headlessui/react' import { useState } from 'react' function PricingPlan() { const plans = [ { id: 'startup', name: 'Startup', price: '$29/month', description: 'Perfect for small teams', features: ['Up to 5 users', '10GB storage', 'Email support'] }, { id: 'business', name: 'Business', price: '$99/month', description: 'For growing companies', features: ['Up to 20 users', '100GB storage', 'Priority support', 'Advanced analytics'] }, { id: 'enterprise', name: 'Enterprise', price: '$299/month', description: 'For large organizations', features: ['Unlimited users', 'Unlimited storage', '24/7 phone support', 'Custom integrations'] }, ] const [selectedPlan, setSelectedPlan] = useState(plans[0]) return ( <div className="w-full max-w-2xl"> <RadioGroup value={selectedPlan} onChange={setSelectedPlan} by="id"> <RadioGroup.Label className="text-lg font-semibold text-gray-900 mb-4"> Select a plan </RadioGroup.Label> <div className="space-y-3"> {plans.map((plan) => ( <RadioGroup.Option key={plan.id} value={plan} className={({ checked, active }) => `${checked ? 'bg-blue-600 text-white' : 'bg-white'} ${active ? 'ring-2 ring-blue-500 ring-offset-2' : ''} relative rounded-lg shadow-md px-5 py-4 cursor-pointer flex focus:outline-none` } > {({ checked }) => ( <div className="flex w-full items-center justify-between"> <div className="flex-1"> <div className="flex items-center justify-between"> <RadioGroup.Label as="p" className={`text-lg font-medium ${checked ? 'text-white' : 'text-gray-900'}`} > {plan.name} </RadioGroup.Label> <span className={`text-lg font-bold ${checked ? 'text-white' : 'text-blue-600'}`}> {plan.price} </span> </div> <RadioGroup.Description as="span" className={`inline text-sm ${checked ? 'text-blue-100' : 'text-gray-500'}`} > {plan.description} </RadioGroup.Description> <ul className={`mt-2 space-y-1 text-sm ${checked ? 'text-blue-50' : 'text-gray-600'}`}> {plan.features.map((feature, index) => ( <li key={index}>• {feature}</li> ))} </ul> </div> {checked && ( <div className="shrink-0 text-white ml-4"> <svg className="h-6 w-6" viewBox="0 0 24 24" fill="none"> <circle cx={12} cy={12} r={12} fill="white" fillOpacity="0.2" /> <path d="M7 13l3 3 7-7" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" /> </svg> </div> )} </div> )} </RadioGroup.Option> ))} </div> </RadioGroup> </div> ) } // Form integration with uncontrolled radio group function DeliveryForm() { return ( <form onSubmit={(e) => { e.preventDefault() const formData = new FormData(e.currentTarget) console.log('Selected delivery:', Object.fromEntries(formData)) // Output: { delivery: "pickup" } }}> <RadioGroup name="delivery" defaultValue="pickup"> <RadioGroup.Label>Delivery method</RadioGroup.Label> <div className="space-y-2 mt-2"> <RadioGroup.Option value="pickup" className={({ checked }) => `px-4 py-2 rounded ${checked ? 'bg-blue-500 text-white' : 'bg-gray-100'}`} > Pickup (Free) </RadioGroup.Option> <RadioGroup.Option value="home-delivery" className={({ checked }) => `px-4 py-2 rounded ${checked ? 'bg-blue-500 text-white' : 'bg-gray-100'}`} > Home delivery ($5) </RadioGroup.Option> <RadioGroup.Option value="dine-in" className={({ checked }) => `px-4 py-2 rounded ${checked ? 'bg-blue-500 text-white' : 'bg-gray-100'}`} > Dine in </RadioGroup.Option> </div> </RadioGroup> <button type="submit" className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"> Continue </button> </form> ) } // Custom comparator for object equality function ObjectRadioGroup() { const options = [ { id: 1, name: 'Alice', role: 'Admin' }, { id: 2, name: 'Bob', role: 'User' }, { id: 3, name: 'Charlie', role: 'Moderator' }, ] const [selected, setSelected] = useState(options[0]) return ( <RadioGroup value={selected} onChange={setSelected} by={(a, z) => a.id === z.id} > <RadioGroup.Label>Select team member</RadioGroup.Label> {options.map((option) => ( <RadioGroup.Option key={option.id} value={option}> {({ checked }) => ( <span className={checked ? 'font-bold' : ''}> {option.name} - {option.role} </span> )} </RadioGroup.Option> ))} </RadioGroup> ) } ``` ## Transition Component CSS transition component for animating enter/leave states ```jsx import { Transition } from '@headlessui/react' import { useState } from 'react' function AnimatedNotification() { const [isShowing, setIsShowing] = useState(false) return ( <> <button onClick={() => setIsShowing(!isShowing)} className="px-4 py-2 bg-blue-500 text-white rounded" > Toggle </button> <Transition show={isShowing} enter="transition-opacity duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="transition-opacity duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="mt-4 p-4 bg-green-100 text-green-900 rounded-lg"> Successfully saved! </div> </Transition> </> ) } // Complex multi-part transition function SlidingPanel() { const [isOpen, setIsOpen] = useState(false) return ( <> <button onClick={() => setIsOpen(!isOpen)}>Open Panel</button> <Transition show={isOpen}> {/* Backdrop */} <Transition.Child enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="fixed inset-0 bg-black/30" onClick={() => setIsOpen(false)} /> </Transition.Child> {/* Panel */} <Transition.Child enter="transition ease-in-out duration-300" enterFrom="translate-x-full" enterTo="translate-x-0" leave="transition ease-in-out duration-300" leaveFrom="translate-x-0" leaveTo="translate-x-full" > <div className="fixed right-0 top-0 h-full w-96 bg-white shadow-xl p-6"> <h2 className="text-xl font-bold">Sliding Panel</h2> <p className="mt-4">Content goes here</p> <button onClick={() => setIsOpen(false)} className="mt-4 px-4 py-2 bg-gray-200 rounded" > Close </button> </div> </Transition.Child> </Transition> </> ) } ``` ## Tailwind CSS Plugin Integration State-based utility variants for styling Headless UI components ```jsx import { Menu } from '@headlessui/react' function StyledMenu() { return ( <Menu> <Menu.Button className="ui-open:bg-blue-500 ui-not-open:bg-gray-500 px-4 py-2 text-white rounded"> Options </Menu.Button> <Menu.Items className="mt-2 bg-white shadow-lg rounded"> <Menu.Item> <a href="/settings" className="ui-active:bg-blue-100 ui-active:text-blue-900 ui-not-active:text-gray-700 block px-4 py-2" > Settings </a> </Menu.Item> <Menu.Item disabled> <span className="ui-disabled:text-gray-400 ui-disabled:cursor-not-allowed block px-4 py-2"> Disabled item </span> </Menu.Item> </Menu.Items> </Menu> ) } // Custom prefix configuration // tailwind.config.js module.exports = { plugins: [ require('@headlessui/tailwindcss')({ prefix: 'ui' }) ] } // Available variants: // ui-open / ui-not-open // ui-checked / ui-not-checked // ui-selected / ui-not-selected // ui-active / ui-not-active // ui-disabled / ui-not-disabled // ui-focus-visible / ui-not-focus-visible function AllVariantsExample() { return ( <> {/* Listbox with state variants */} <Listbox> <Listbox.Button className="ui-open:rotate-180">▼</Listbox.Button> <Listbox.Options> <Listbox.Option value="a" className="ui-active:bg-blue-500 ui-selected:font-bold ui-disabled:opacity-50" > Option A </Listbox.Option> </Listbox.Options> </Listbox> {/* Switch with checked variants */} <Switch className="ui-checked:bg-green-500 ui-not-checked:bg-gray-300 ui-focus-visible:ring-2"> Toggle </Switch> </> ) } ``` ## Summary Headless UI provides a comprehensive suite of accessible, unstyled UI components that handle the complex logic of interactive interfaces while giving developers complete styling freedom. The library excels at solving common challenges in modern web applications including keyboard navigation, focus management, screen reader support, and state synchronization. Each component follows WAI-ARIA design patterns and best practices, ensuring that applications built with Headless UI are accessible by default without requiring deep accessibility expertise from developers. The v2.2.9 release includes enhanced form components like Button, Checkbox, Input, Select, and Textarea that integrate seamlessly with native HTML forms while providing rich state management and accessibility features. The component API is consistent across the library, using render props to expose internal state for conditional styling, supporting both controlled and uncontrolled modes for flexibility, and integrating seamlessly with HTML forms for data submission. The Tailwind CSS plugin extends this integration by providing utility variants that respond to component state, enabling declarative styling patterns that keep component logic and visual presentation clearly separated. Whether building dropdown menus, modal dialogs, autocomplete inputs, tabbed interfaces, or accessible forms, Headless UI provides the foundation for creating polished, accessible user experiences with minimal complexity and maximum control. The library's focus on providing functionality without styling constraints makes it particularly well-suited for design systems and component libraries that require precise visual control.