A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.
84
Components for organizing and navigating content including dropdown menus, tab interfaces, and collapsible disclosure panels.
A dropdown menu component for navigation and actions with keyboard navigation and accessibility.
/**
* Dropdown menu component for actions and navigation
* @param props - Menu properties
*/
function Menu<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: MenuProps<TTag>
): JSX.Element;
interface MenuProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Demo mode flag for development */
__demoMode?: boolean;
/** Render prop providing menu state */
children?: React.ReactNode | ((props: MenuRenderProps) => React.ReactNode);
}
interface MenuRenderProps {
/** Whether menu is open */
open: boolean;
/** Function to close menu */
close: () => void;
}Button to toggle the menu display.
/**
* Button to toggle the menu
* @param props - MenuButton properties
*/
function MenuButton<TTag extends keyof JSX.IntrinsicElements = 'button'>(
props: MenuButtonProps<TTag>
): JSX.Element;
interface MenuButtonProps<TTag extends keyof JSX.IntrinsicElements = 'button'>
extends PolymorphicProps<TTag> {
/** Whether button is disabled */
disabled?: boolean;
/** Whether to auto-focus on mount */
autoFocus?: boolean;
/** Render prop providing button state */
children?: React.ReactNode | ((props: MenuButtonRenderProps) => React.ReactNode);
}
interface MenuButtonRenderProps {
/** Whether menu is open */
open: boolean;
/** Whether button is active/pressed */
active: boolean;
/** Whether being hovered */
hover: boolean;
/** Whether has focus */
focus: boolean;
/** Whether disabled */
disabled: boolean;
/** Whether has autofocus */
autofocus: boolean;
}Container for menu items with keyboard navigation.
/**
* Container for menu items
* @param props - MenuItems properties
*/
function MenuItems<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: MenuItemsProps<TTag>
): JSX.Element;
interface MenuItemsProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to hold focus on items */
hold?: boolean;
/** Floating UI anchor configuration */
anchor?: AnchorProps;
/** Whether to render in portal */
portal?: boolean;
/** Whether to use modal behavior */
modal?: boolean;
/** Whether to use transition animations */
transition?: boolean;
/** Render prop providing items state */
children?: React.ReactNode | ((props: MenuItemsRenderProps) => React.ReactNode);
}
interface MenuItemsRenderProps {
/** Whether menu is open */
open: boolean;
}Individual item within the menu.
/**
* Individual item within the menu
* @param props - MenuItem properties
*/
function MenuItem<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: MenuItemProps<TTag>
): JSX.Element;
interface MenuItemProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether item is disabled */
disabled?: boolean;
/** Display order */
order?: number;
/** Render prop providing item state */
children?: React.ReactNode | ((props: MenuItemRenderProps) => React.ReactNode);
}
interface MenuItemRenderProps {
/** Whether item has focus */
focus: boolean;
/** Whether item is active (deprecated, use focus) */
active: boolean;
/** Whether item is disabled */
disabled: boolean;
/** Function to close menu */
close: () => void;
}Heading component for menu sections.
/**
* Heading for menu sections
* @param props - MenuHeading properties
*/
function MenuHeading<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: MenuHeadingProps<TTag>
): JSX.Element;
interface MenuHeadingProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Standard heading properties */
}Section grouping component for menu items.
/**
* Section grouping for menu items
* @param props - MenuSection properties
*/
function MenuSection<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: MenuSectionProps<TTag>
): JSX.Element;
interface MenuSectionProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Standard section properties */
}Visual separator between menu items or sections.
/**
* Visual separator between menu items
* @param props - MenuSeparator properties
*/
function MenuSeparator<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: MenuSeparatorProps<TTag>
): JSX.Element;
interface MenuSeparatorProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Standard separator properties */
}Usage Examples:
import {
Menu,
MenuButton,
MenuItems,
MenuItem,
MenuHeading,
MenuSection,
MenuSeparator
} from "@headlessui/react";
function BasicMenuExample() {
return (
<Menu>
<MenuButton className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Options
</MenuButton>
<MenuItems
anchor="bottom end"
className="w-52 bg-white border border-gray-200 rounded-lg shadow-lg p-1 mt-1"
>
<MenuItem>
{({ focus }) => (
<button
className={`${focus ? 'bg-blue-500 text-white' : 'text-gray-900'}
group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
Edit
</button>
)}
</MenuItem>
<MenuItem>
{({ focus }) => (
<button
className={`${focus ? 'bg-blue-500 text-white' : 'text-gray-900'}
group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
Duplicate
</button>
)}
</MenuItem>
<MenuSeparator className="my-1 h-px bg-gray-200" />
<MenuItem disabled>
{({ focus, disabled }) => (
<button
className={`${disabled ? 'text-gray-400' : focus ? 'bg-red-500 text-white' : 'text-red-600'}
group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
Delete (disabled)
</button>
)}
</MenuItem>
</MenuItems>
</Menu>
);
}
// Complex menu with sections and headings
function ComplexMenuExample() {
return (
<Menu>
<MenuButton className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded">
<img className="w-8 h-8 rounded-full" src="/profile.jpg" alt="Profile" />
<span>John Doe</span>
<ChevronDownIcon className="w-4 h-4" />
</MenuButton>
<MenuItems
anchor="bottom end"
className="w-64 bg-white border border-gray-200 rounded-lg shadow-lg p-2 mt-2"
>
<MenuSection>
<MenuHeading className="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Account
</MenuHeading>
<MenuItem>
{({ focus, close }) => (
<a
href="/profile"
onClick={() => close()}
className={`${focus ? 'bg-gray-100' : ''}
flex items-center px-3 py-2 text-sm text-gray-700 rounded-md`}
>
<UserIcon className="w-4 h-4 mr-3" />
Your Profile
</a>
)}
</MenuItem>
<MenuItem>
{({ focus, close }) => (
<a
href="/settings"
onClick={() => close()}
className={`${focus ? 'bg-gray-100' : ''}
flex items-center px-3 py-2 text-sm text-gray-700 rounded-md`}
>
<CogIcon className="w-4 h-4 mr-3" />
Settings
</a>
)}
</MenuItem>
</MenuSection>
<MenuSeparator className="my-2 h-px bg-gray-200" />
<MenuSection>
<MenuHeading className="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Actions
</MenuHeading>
<MenuItem>
{({ focus, close }) => (
<button
onClick={() => {
console.log('Signing out...');
close();
}}
className={`${focus ? 'bg-red-50 text-red-700' : 'text-red-600'}
flex items-center w-full px-3 py-2 text-sm rounded-md`}
>
<ArrowRightOnRectangleIcon className="w-4 h-4 mr-3" />
Sign out
</button>
)}
</MenuItem>
</MenuSection>
</MenuItems>
</Menu>
);
}A tab interface component for organizing content into multiple panels.
/**
* Tab interface for organizing content
* @param props - TabGroup properties
*/
function TabGroup<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: TabGroupProps<TTag>
): JSX.Element;
interface TabGroupProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Currently selected tab index */
selectedIndex?: number;
/** Default selected index for uncontrolled usage */
defaultIndex?: number;
/** Selection change handler */
onChange?: (index: number) => void;
/** Whether tabs are vertically oriented */
vertical?: boolean;
/** Whether tab selection requires manual activation (not automatic on focus) */
manual?: boolean;
/** Render prop providing tab group state */
children?: React.ReactNode | ((props: TabGroupRenderProps) => React.ReactNode);
}
interface TabGroupRenderProps {
/** Currently selected tab index */
selectedIndex: number;
}Container for tab buttons.
/**
* Container for tab buttons
* @param props - TabList properties
*/
function TabList<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: TabListProps<TTag>
): JSX.Element;
interface TabListProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Render prop providing tab list state */
children?: React.ReactNode | ((props: TabListRenderProps) => React.ReactNode);
}
interface TabListRenderProps {
/** Currently selected tab index */
selectedIndex: number;
}Individual tab button.
/**
* Individual tab button
* @param props - Tab properties
*/
function Tab<TTag extends keyof JSX.IntrinsicElements = 'button'>(
props: TabProps<TTag>
): JSX.Element;
interface TabProps<TTag extends keyof JSX.IntrinsicElements = 'button'>
extends PolymorphicProps<TTag> {
/** Whether tab is disabled */
disabled?: boolean;
/** Whether to auto-focus on mount */
autoFocus?: boolean;
/** Render prop providing tab state */
children?: React.ReactNode | ((props: TabRenderProps) => React.ReactNode);
}
interface TabRenderProps {
/** Whether tab is selected */
selected: boolean;
/** Whether being hovered */
hover: boolean;
/** Whether has focus */
focus: boolean;
/** Whether being pressed */
active: boolean;
/** Whether has autofocus */
autofocus: boolean;
/** Whether disabled */
disabled: boolean;
}Container for tab panel content.
/**
* Container for tab panels
* @param props - TabPanels properties
*/
function TabPanels<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: TabPanelsProps<TTag>
): JSX.Element;
interface TabPanelsProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Render prop providing tab panels state */
children?: React.ReactNode | ((props: TabPanelsRenderProps) => React.ReactNode);
}
interface TabPanelsRenderProps {
/** Currently selected tab index */
selectedIndex: number;
}Individual tab panel content.
/**
* Individual tab panel content
* @param props - TabPanel properties
*/
function TabPanel<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: TabPanelProps<TTag>
): JSX.Element;
interface TabPanelProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Tab index for keyboard navigation */
tabIndex?: number;
/** Render prop providing panel state */
children?: React.ReactNode | ((props: TabPanelRenderProps) => React.ReactNode);
}
interface TabPanelRenderProps {
/** Whether panel is selected/visible */
selected: boolean;
/** Whether panel has focus */
focus: boolean;
}Usage Examples:
import { useState } from "react";
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@headlessui/react";
function BasicTabsExample() {
const categories = ['Recent', 'Popular', 'Trending'];
return (
<div className="w-full max-w-md mx-auto">
<TabGroup>
<TabList className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
{categories.map((category) => (
<Tab
key={category}
className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium leading-5
ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2
${selected
? 'bg-white text-blue-700 shadow'
: 'text-blue-100 hover:bg-white/[0.12] hover:text-white'
}`
}
>
{category}
</Tab>
))}
</TabList>
<TabPanels className="mt-2">
<TabPanel className="rounded-xl bg-white p-3">
<h3 className="text-lg font-semibold">Recent Posts</h3>
<p className="mt-2 text-gray-600">Content for recent posts...</p>
</TabPanel>
<TabPanel className="rounded-xl bg-white p-3">
<h3 className="text-lg font-semibold">Popular Posts</h3>
<p className="mt-2 text-gray-600">Content for popular posts...</p>
</TabPanel>
<TabPanel className="rounded-xl bg-white p-3">
<h3 className="text-lg font-semibold">Trending Posts</h3>
<p className="mt-2 text-gray-600">Content for trending posts...</p>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
}
// Controlled tabs
function ControlledTabsExample() {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<TabGroup selectedIndex={selectedIndex} onChange={setSelectedIndex}>
<TabList className="flex border-b border-gray-200">
<Tab className={({ selected }) =>
`px-4 py-2 border-b-2 ${selected ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500'}`
}>
Profile
</Tab>
<Tab className={({ selected }) =>
`px-4 py-2 border-b-2 ${selected ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500'}`
}>
Settings
</Tab>
</TabList>
<TabPanels>
<TabPanel className="p-4">
<p>Selected tab: {selectedIndex}</p>
<p>Profile content goes here...</p>
</TabPanel>
<TabPanel className="p-4">
<p>Settings content goes here...</p>
</TabPanel>
</TabPanels>
</TabGroup>
);
}
// Vertical tabs
function VerticalTabsExample() {
return (
<TabGroup vertical className="flex">
<TabList className="flex flex-col w-48 border-r border-gray-200">
<Tab className={({ selected }) =>
`px-4 py-3 text-left ${selected ? 'bg-blue-50 border-r-2 border-blue-500 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`
}>
General
</Tab>
<Tab className={({ selected }) =>
`px-4 py-3 text-left ${selected ? 'bg-blue-50 border-r-2 border-blue-500 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`
}>
Security
</Tab>
<Tab className={({ selected }) =>
`px-4 py-3 text-left ${selected ? 'bg-blue-50 border-r-2 border-blue-500 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`
}>
Notifications
</Tab>
</TabList>
<TabPanels className="flex-1 p-6">
<TabPanel>
<h2 className="text-xl font-semibold mb-4">General Settings</h2>
<p>General settings content...</p>
</TabPanel>
<TabPanel>
<h2 className="text-xl font-semibold mb-4">Security Settings</h2>
<p>Security settings content...</p>
</TabPanel>
<TabPanel>
<h2 className="text-xl font-semibold mb-4">Notification Settings</h2>
<p>Notification settings content...</p>
</TabPanel>
</TabPanels>
</TabGroup>
);
}A collapsible disclosure component for showing and hiding content.
/**
* Collapsible disclosure component
* @param props - Disclosure properties
*/
function Disclosure<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: DisclosureProps<TTag>
): JSX.Element;
interface DisclosureProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Default open state for uncontrolled usage */
defaultOpen?: boolean;
/** Render prop providing disclosure state */
children?: React.ReactNode | ((props: DisclosureRenderProps) => React.ReactNode);
}
interface DisclosureRenderProps {
/** Whether disclosure is open */
open: boolean;
/** Function to close disclosure */
close: (focusableElement?: HTMLElement | React.MutableRefObject<HTMLElement | null>) => void;
}Button to toggle the disclosure state.
/**
* Button to toggle the disclosure
* @param props - DisclosureButton properties
*/
function DisclosureButton<TTag extends keyof JSX.IntrinsicElements = 'button'>(
props: DisclosureButtonProps<TTag>
): JSX.Element;
interface DisclosureButtonProps<TTag extends keyof JSX.IntrinsicElements = 'button'>
extends PolymorphicProps<TTag> {
/** Whether button is disabled */
disabled?: boolean;
/** Whether to auto-focus on mount */
autoFocus?: boolean;
/** Render prop providing button state */
children?: React.ReactNode | ((props: DisclosureButtonRenderProps) => React.ReactNode);
}
interface DisclosureButtonRenderProps {
/** Whether disclosure is open */
open: boolean;
/** Whether being hovered */
hover: boolean;
/** Whether being pressed */
active: boolean;
/** Whether disabled */
disabled: boolean;
/** Whether has focus */
focus: boolean;
/** Whether has autofocus */
autofocus: boolean;
}The collapsible content panel.
/**
* Collapsible content panel
* @param props - DisclosurePanel properties
*/
function DisclosurePanel<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: DisclosurePanelProps<TTag>
): JSX.Element;
interface DisclosurePanelProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to use transition animations */
transition?: boolean;
/** Render prop providing panel state */
children?: React.ReactNode | ((props: DisclosurePanelRenderProps) => React.ReactNode);
}
interface DisclosurePanelRenderProps {
/** Whether disclosure is open */
open: boolean;
/** Function to close disclosure */
close: (focusableElement?: HTMLElement | React.MutableRefObject<HTMLElement | null>) => void;
}Usage Examples:
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/react";
function BasicDisclosureExample() {
return (
<Disclosure>
<DisclosureButton className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500/75">
{({ open }) => (
<>
<span>What is your refund policy?</span>
<ChevronUpIcon
className={`${open ? 'rotate-180' : ''} h-5 w-5 text-purple-500 transition-transform`}
/>
</>
)}
</DisclosureButton>
<DisclosurePanel className="px-4 pb-2 pt-4 text-sm text-gray-500">
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.
</DisclosurePanel>
</Disclosure>
);
}
// FAQ with multiple disclosures
function FAQExample() {
const faqs = [
{
question: "What's the best thing about Switzerland?",
answer: "I don't know, but the flag is a big plus. Lorem ipsum dolor sit amet consectetur adipisicing elit."
},
{
question: "How do you make holy water?",
answer: "You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit."
},
{
question: "Why do you never see elephants hiding in trees?",
answer: "Because they're so good at it. Lorem ipsum dolor sit amet consectetur adipisicing elit."
}
];
return (
<div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2">
{faqs.map((faq, index) => (
<Disclosure key={index} as="div" className="mt-2">
<DisclosureButton className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500/75">
<span>{faq.question}</span>
<ChevronUpIcon className="h-5 w-5 text-purple-500 ui-open:rotate-180 ui-open:transform" />
</DisclosureButton>
<DisclosurePanel className="px-4 pb-2 pt-4 text-sm text-gray-500">
{faq.answer}
</DisclosurePanel>
</Disclosure>
))}
</div>
);
}
// Disclosure with transitions
function AnimatedDisclosureExample() {
return (
<Disclosure>
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg bg-gray-100 px-4 py-2">
<span className="text-sm font-medium">Animated Disclosure</span>
<ChevronDownIcon className="w-5 h-5 group-data-[open]:rotate-180 transition-transform" />
</DisclosureButton>
<DisclosurePanel
transition
className="origin-top transition duration-200 ease-out data-[closed]:-translate-y-6 data-[closed]:opacity-0"
>
<div className="px-4 py-2 text-sm text-gray-600">
This content animates in and out smoothly when the disclosure is toggled.
</div>
</DisclosurePanel>
</Disclosure>
);
}// Floating UI anchor configuration for menu positioning
interface AnchorProps {
to?: string;
gap?: number;
offset?: number;
padding?: number;
}
// Common navigation render props
interface BaseNavigationRenderProps {
open: boolean;
close: () => void;
}
// Tab selection change handler
type TabChangeHandler = (index: number) => void;
// Close function with optional focus target
type CloseFunction = (
focusableElement?: HTMLElement | React.MutableRefObject<HTMLElement | null>
) => void;All navigation components include full keyboard support:
Components automatically include appropriate ARIA attributes:
Install with Tessl CLI
npx tessl i tessl/npm-headlessui--reactdocs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10