headless-ui.txtā¢10.3 kB
# Headless UI - Unstyled, Accessible UI Components
## Overview
Headless UI provides unstyled, fully accessible UI components designed to integrate with Tailwind CSS. Built by the Tailwind team.
## Installation
```bash
npm install @headlessui/react
```
## Key Components
### Menu (Dropdown)
```tsx
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
function MyDropdown() {
return (
<Menu>
<MenuButton className="inline-flex items-center gap-2 rounded-md bg-gray-800 py-1.5 px-3 text-sm/6 font-semibold text-white">
Options
<ChevronDownIcon className="size-4" />
</MenuButton>
<MenuItems className="absolute right-0 mt-2 w-52 origin-top-right rounded-md bg-white shadow-lg">
<MenuItem>
{({ focus }) => (
<button className={`${focus ? 'bg-blue-500 text-white' : 'text-gray-900'} group flex w-full items-center px-2 py-2`}>
Edit
</button>
)}
</MenuItem>
<MenuItem>
{({ focus }) => (
<button className={`${focus ? 'bg-blue-500 text-white' : 'text-gray-900'} group flex w-full items-center px-2 py-2`}>
Delete
</button>
)}
</MenuItem>
</MenuItems>
</Menu>
)
}
```
### Dialog (Modal)
```tsx
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react'
import { useState } from 'react'
function MyModal() {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)} className="relative z-50">
{/* Backdrop */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
{/* Full-screen container */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel className="mx-auto max-w-sm rounded bg-white p-6">
<DialogTitle className="text-lg font-bold">Modal Title</DialogTitle>
<p className="mt-2">Modal content goes here</p>
<button onClick={() => setIsOpen(false)} className="mt-4">Close</button>
</DialogPanel>
</div>
</Dialog>
</>
)
}
```
### Popover
```tsx
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
function MyPopover() {
return (
<Popover className="relative">
<PopoverButton className="text-white/90 focus:outline-none">
Solutions
</PopoverButton>
<PopoverPanel className="absolute z-10 mt-3 w-screen max-w-sm">
<div className="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<a href="/analytics" className="block rounded p-2 hover:bg-gray-100">
Analytics
</a>
<a href="/engagement" className="block rounded p-2 hover:bg-gray-100">
Engagement
</a>
</div>
</div>
</PopoverPanel>
</Popover>
)
}
```
### Listbox (Select)
```tsx
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
]
function MySelect() {
const [selected, setSelected] = useState(people[0])
return (
<Listbox value={selected} onChange={setSelected}>
<ListboxButton>{selected.name}</ListboxButton>
<ListboxOptions>
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="cursor-pointer">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
```
### Switch (Toggle)
```tsx
import { Switch } from '@headlessui/react'
import { useState } from 'react'
function MyToggle() {
const [enabled, setEnabled] = useState(false)
return (
<Switch
checked={enabled}
onChange={setEnabled}
className={`${enabled ? 'bg-blue-600' : 'bg-gray-200'} relative inline-flex h-6 w-11 items-center rounded-full`}
>
<span className={`${enabled ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition`} />
</Switch>
)
}
```
### Tabs
```tsx
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
function MyTabs() {
return (
<TabGroup>
<TabList className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
<Tab className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium ${
selected ? 'bg-white shadow' : 'text-blue-100 hover:bg-white/[0.12]'
}`
}>
Recent
</Tab>
<Tab className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium ${
selected ? 'bg-white shadow' : 'text-blue-100 hover:bg-white/[0.12]'
}`
}>
Popular
</Tab>
</TabList>
<TabPanels className="mt-2">
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
</TabPanels>
</TabGroup>
)
}
```
### Disclosure (Accordion)
```tsx
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { ChevronUpIcon } from '@heroicons/react/20/solid'
function MyAccordion() {
return (
<Disclosure>
{({ open }) => (
<>
<DisclosureButton className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2">
<span>What is your refund policy?</span>
<ChevronUpIcon className={`${open ? 'rotate-180 transform' : ''} h-5 w-5`} />
</DisclosureButton>
<DisclosurePanel className="px-4 pt-4 pb-2 text-sm text-gray-500">
If you're unhappy with your purchase, we'll refund you in full.
</DisclosurePanel>
</>
)}
</Disclosure>
)
}
```
### Combobox (Autocomplete)
```tsx
import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from '@headlessui/react'
import { useState } from 'react'
function MyAutocomplete() {
const [query, setQuery] = useState('')
const [selected, setSelected] = useState(null)
const people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
]
const filtered =
query === ''
? people
: people.filter((person) =>
person.name.toLowerCase().includes(query.toLowerCase())
)
return (
<Combobox value={selected} onChange={setSelected}>
<ComboboxInput
onChange={(event) => setQuery(event.target.value)}
displayValue={(person) => person?.name}
/>
<ComboboxOptions>
{filtered.map((person) => (
<ComboboxOption key={person.id} value={person}>
{person.name}
</ComboboxOption>
))}
</ComboboxOptions>
</Combobox>
)
}
```
### Radio Group
```tsx
import { RadioGroup, RadioGroupOption } from '@headlessui/react'
import { useState } from 'react'
const plans = ['Startup', 'Business', 'Enterprise']
function MyRadioGroup() {
const [selected, setSelected] = useState(plans[0])
return (
<RadioGroup value={selected} onChange={setSelected}>
{plans.map((plan) => (
<RadioGroupOption key={plan} value={plan} className={({ checked }) =>
`${checked ? 'bg-blue-900 text-white' : 'bg-white'} relative flex cursor-pointer rounded-lg px-5 py-4 shadow-md focus:outline-none`
}>
{plan}
</RadioGroupOption>
))}
</RadioGroup>
)
}
```
## Transitions
```tsx
import { Transition } from '@headlessui/react'
import { useState } from 'react'
function MyTransition() {
const [isShowing, setIsShowing] = useState(false)
return (
<>
<button onClick={() => setIsShowing(!isShowing)}>Toggle</button>
<Transition
show={isShowing}
enter="transition-opacity duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="p-4 bg-blue-500 text-white rounded">
I will fade in and out
</div>
</Transition>
</>
)
}
```
## Common Patterns
### Mobile Navigation
```tsx
<Disclosure as="nav">
{({ open }) => (
<>
<DisclosureButton>Menu</DisclosureButton>
<DisclosurePanel>
<a href="/">Home</a>
<a href="/about">About</a>
</DisclosurePanel>
</>
)}
</Disclosure>
```
### Settings Panel
```tsx
<Dialog>
<DialogPanel>
<TabGroup>
<TabList>
<Tab>General</Tab>
<Tab>Privacy</Tab>
</TabList>
<TabPanels>
<TabPanel>General settings</TabPanel>
<TabPanel>Privacy settings</TabPanel>
</TabPanels>
</TabGroup>
</DialogPanel>
</Dialog>
```
### Form with Validation
```tsx
<Listbox value={selected} onChange={setSelected}>
<ListboxButton className="border rounded px-4 py-2">
{selected.name}
</ListboxButton>
<ListboxOptions>
{options.map((option) => (
<ListboxOption key={option.id} value={option}>
{({ selected, focus }) => (
<div className={`${focus ? 'bg-blue-100' : ''} ${selected ? 'font-bold' : ''}`}>
{option.name}
</div>
)}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
```
## Best Practices
1. **Use render props** for dynamic styling based on state
2. **Combine with Tailwind** for easy styling
3. **Accessible by default** - includes proper ARIA attributes
4. **Keyboard navigation** - built-in keyboard support
5. **Focus management** - automatic focus handling
## With TypeScript
```tsx
import { Dialog } from '@headlessui/react'
interface MyModalProps {
isOpen: boolean
onClose: () => void
title: string
}
function MyModal({ isOpen, onClose, title }: MyModalProps) {
return (
<Dialog open={isOpen} onClose={onClose}>
{/* Modal content */}
</Dialog>
)
}
```
## Resources
- Docs: https://headlessui.com/
- Examples: https://headlessui.com/react/menu#examples
- GitHub: https://github.com/tailwindlabs/headlessui