A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.
84
Modal dialogs, popovers, and tooltips that appear on top of the main content with focus management, portal rendering, and backdrop support.
A modal dialog component with focus trapping, backdrop support, and automatic accessibility features.
/**
* Modal dialog component with focus management
* @param props - Dialog properties including open state and close handler
*/
function Dialog<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: DialogProps<TTag>
): JSX.Element;
interface DialogProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether dialog is open */
open?: boolean;
/** Close handler - called when dialog should close */
onClose: (value: boolean) => void;
/** Initial focus target element */
initialFocus?: React.MutableRefObject<HTMLElement | null>;
/** ARIA role for the dialog */
role?: 'dialog' | 'alertdialog';
/** Whether to auto-focus on open */
autoFocus?: boolean;
/** Whether to use transition animations */
transition?: boolean;
/** Demo mode flag for development */
__demoMode?: boolean;
/** Render prop providing dialog state */
children?: React.ReactNode | ((props: DialogRenderProps) => React.ReactNode);
}
interface DialogRenderProps {
/** Whether dialog is open */
open: boolean;
}The main content container of the dialog.
/**
* Main content container of the dialog
* @param props - DialogPanel properties
*/
function DialogPanel<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: DialogPanelProps<TTag>
): JSX.Element;
interface DialogPanelProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to use transition animations */
transition?: boolean;
/** Render prop providing dialog state */
children?: React.ReactNode | ((props: DialogPanelRenderProps) => React.ReactNode);
}
interface DialogPanelRenderProps {
/** Whether dialog is open */
open: boolean;
}The backdrop/overlay behind the dialog content.
/**
* Backdrop/overlay behind the dialog
* @param props - DialogBackdrop properties
*/
function DialogBackdrop<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: DialogBackdropProps<TTag>
): JSX.Element;
interface DialogBackdropProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to use transition animations */
transition?: boolean;
/** Render prop providing dialog state */
children?: React.ReactNode | ((props: DialogBackdropRenderProps) => React.ReactNode);
}
interface DialogBackdropRenderProps {
/** Whether dialog is open */
open: boolean;
}The title element of the dialog with automatic ARIA labeling.
/**
* Title element of the dialog
* @param props - DialogTitle properties
*/
function DialogTitle<TTag extends keyof JSX.IntrinsicElements = 'h2'>(
props: DialogTitleProps<TTag>
): JSX.Element;
interface DialogTitleProps<TTag extends keyof JSX.IntrinsicElements = 'h2'>
extends PolymorphicProps<TTag> {
/** Render prop providing dialog state */
children?: React.ReactNode | ((props: DialogTitleRenderProps) => React.ReactNode);
}
interface DialogTitleRenderProps {
/** Whether dialog is open */
open: boolean;
}Usage Examples:
import { useState } from "react";
import {
Dialog,
DialogPanel,
DialogTitle,
DialogBackdrop,
Button,
CloseButton
} from "@headlessui/react";
function DialogExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>
Open Dialog
</Button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
{/* Backdrop */}
<DialogBackdrop className="fixed inset-0 bg-black/30" />
{/* Container to center the panel */}
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel className="max-w-lg space-y-4 bg-white p-6 rounded-lg shadow-xl">
<DialogTitle className="font-bold text-xl">
Confirm Delete
</DialogTitle>
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
<div className="flex gap-4">
<Button
onClick={() => setIsOpen(false)}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
>
Cancel
</Button>
<Button
onClick={() => {
// Handle delete
setIsOpen(false);
}}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Delete
</Button>
</div>
{/* Close button in corner */}
<CloseButton className="absolute top-4 right-4 text-gray-400 hover:text-gray-600">
<XMarkIcon className="w-5 h-5" />
</CloseButton>
</DialogPanel>
</div>
</Dialog>
</>
);
}
// Dialog with transition animations
function AnimatedDialogExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onClose={() => setIsOpen(false)} transition>
<DialogBackdrop
transition
className="fixed inset-0 bg-black/30 duration-300 ease-out data-[closed]:opacity-0"
/>
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
<DialogPanel
transition
className="max-w-lg bg-white p-6 rounded-lg shadow-xl duration-300 ease-out
data-[closed]:transform data-[closed]:scale-95 data-[closed]:opacity-0"
>
<DialogTitle>Animated Dialog</DialogTitle>
<p>This dialog has smooth animations.</p>
</DialogPanel>
</div>
</Dialog>
);
}A popover component for displaying floating content triggered by user interaction.
/**
* Popover component for floating content
* @param props - Popover properties
*/
function Popover<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: PopoverProps<TTag>
): JSX.Element;
interface PopoverProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Demo mode flag for development */
__demoMode?: boolean;
/** Render prop providing popover state */
children?: React.ReactNode | ((props: PopoverRenderProps) => React.ReactNode);
}
interface PopoverRenderProps {
/** Whether popover is open */
open: boolean;
/** Function to close popover */
close: (focusableElement?: HTMLElement | React.MutableRefObject<HTMLElement | null>) => void;
}Button to toggle the popover display.
/**
* Button to toggle the popover
* @param props - PopoverButton properties
*/
function PopoverButton<TTag extends keyof JSX.IntrinsicElements = 'button'>(
props: PopoverButtonProps<TTag>
): JSX.Element;
interface PopoverButtonProps<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: PopoverButtonRenderProps) => React.ReactNode);
}
interface PopoverButtonRenderProps {
/** Whether popover 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;
}The floating panel content of the popover.
/**
* Floating panel content of the popover
* @param props - PopoverPanel properties
*/
function PopoverPanel<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: PopoverPanelProps<TTag>
): JSX.Element;
interface PopoverPanelProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether panel should receive focus */
focus?: 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 panel state */
children?: React.ReactNode | ((props: PopoverPanelRenderProps) => React.ReactNode);
}
interface PopoverPanelRenderProps {
/** Whether popover is open */
open: boolean;
/** Function to close popover */
close: (focusableElement?: HTMLElement | React.MutableRefObject<HTMLElement | null>) => void;
}Groups multiple popovers together for coordinated behavior.
/**
* Groups multiple popovers together
* @param props - PopoverGroup properties
*/
function PopoverGroup<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: PopoverGroupProps<TTag>
): JSX.Element;
interface PopoverGroupProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Standard group properties */
}Backdrop component for popover overlays.
/**
* Backdrop for popover overlay
* @param props - PopoverBackdrop properties
*/
function PopoverBackdrop<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: PopoverBackdropProps<TTag>
): JSX.Element;
interface PopoverBackdropProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to use transition animations */
transition?: boolean;
/** Render prop providing backdrop state */
children?: React.ReactNode | ((props: PopoverBackdropRenderProps) => React.ReactNode);
}
interface PopoverBackdropRenderProps {
/** Whether popover is open */
open: boolean;
}Overlay component for popover modal behavior.
/**
* Overlay component for popover
* @param props - PopoverOverlay properties
*/
function PopoverOverlay<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: PopoverOverlayProps<TTag>
): JSX.Element;
interface PopoverOverlayProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to use transition animations */
transition?: boolean;
/** Render prop providing overlay state */
children?: React.ReactNode | ((props: PopoverOverlayRenderProps) => React.ReactNode);
}
interface PopoverOverlayRenderProps {
/** Whether popover is open */
open: boolean;
}Usage Examples:
import {
Popover,
PopoverButton,
PopoverPanel,
PopoverGroup
} from "@headlessui/react";
function PopoverExample() {
return (
<Popover>
<PopoverButton className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Options
</PopoverButton>
<PopoverPanel
anchor="bottom start"
className="flex flex-col bg-white border border-gray-200 rounded-lg shadow-lg p-2 mt-2"
>
{({ close }) => (
<>
<button
onClick={() => {
console.log('Edit clicked');
close();
}}
className="px-3 py-2 text-left hover:bg-gray-100 rounded"
>
Edit
</button>
<button
onClick={() => {
console.log('Delete clicked');
close();
}}
className="px-3 py-2 text-left hover:bg-gray-100 rounded text-red-600"
>
Delete
</button>
</>
)}
</PopoverPanel>
</Popover>
);
}
// Navigation popover with grouped behavior
function NavigationPopover() {
const solutions = [
{ name: 'Analytics', description: 'Get insights into your traffic' },
{ name: 'Engagement', description: 'Speak directly to your customers' },
{ name: 'Security', description: 'Keep your data safe and secure' },
];
return (
<PopoverGroup className="flex space-x-10">
<Popover>
<PopoverButton className="text-gray-900 hover:text-gray-700">
Solutions
</PopoverButton>
<PopoverPanel
anchor="bottom start"
className="absolute z-10 mt-3 w-screen max-w-md transform px-2 sm:px-0"
>
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid gap-6 bg-white px-5 py-6 sm:gap-8 sm:p-8">
{solutions.map((item) => (
<a
key={item.name}
href="#"
className="-m-3 flex items-start rounded-lg p-3 hover:bg-gray-50"
>
<div className="ml-4">
<p className="text-base font-medium text-gray-900">
{item.name}
</p>
<p className="mt-1 text-sm text-gray-500">
{item.description}
</p>
</div>
</a>
))}
</div>
</div>
</PopoverPanel>
</Popover>
{/* Additional popovers in the group */}
<Popover>
<PopoverButton className="text-gray-900 hover:text-gray-700">
Pricing
</PopoverButton>
<PopoverPanel anchor="bottom start">
{/* Pricing content */}
</PopoverPanel>
</Popover>
</PopoverGroup>
);
}
// Modal popover with backdrop
function ModalPopoverExample() {
return (
<Popover>
<PopoverButton>Open Modal Popover</PopoverButton>
<PopoverBackdrop className="fixed inset-0 bg-black/20" />
<PopoverPanel
modal
className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
bg-white p-6 rounded-lg shadow-xl max-w-sm w-full mx-4"
>
<h3 className="text-lg font-medium mb-4">Modal Popover</h3>
<p className="text-gray-600 mb-4">This popover has modal behavior with a backdrop.</p>
{({ close }) => (
<button
onClick={() => close()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Close
</button>
)}
</PopoverPanel>
</Popover>
);
}Note: Tooltip component is commented out in the current version (v2.2.7) but may be available in future versions.
// TODO: Enable when ready (from source comments)
// function Tooltip(props: TooltipProps): JSX.Element;// Floating UI anchor configuration for positioning
interface AnchorProps {
/** Anchor position relative to trigger */
to?:
| 'top' | 'top start' | 'top end'
| 'right' | 'right start' | 'right end'
| 'bottom' | 'bottom start' | 'bottom end'
| 'left' | 'left start' | 'left end';
/** Gap between trigger and floating element */
gap?: number;
/** Offset from default position */
offset?: number;
/** Padding from viewport edges */
padding?: number;
}
// Common close function type for overlay components
type CloseFunction = (
focusableElement?: HTMLElement | React.MutableRefObject<HTMLElement | null>
) => void;
// Base overlay render props
interface BaseOverlayRenderProps {
open: boolean;
close: CloseFunction;
}All overlay components implement proper focus management:
Overlay components can render in portals to avoid z-index and overflow issues:
// Render in portal (outside normal DOM tree)
<DialogPanel portal className="...">
Content renders at document body level
</DialogPanel>
// Render in-place (default)
<DialogPanel className="...">
Content renders where declared
</DialogPanel>All overlay components support smooth transitions:
<Dialog open={isOpen} onClose={setIsOpen} transition>
<DialogBackdrop
transition
className="duration-300 ease-out data-[closed]:opacity-0"
/>
<DialogPanel
transition
className="duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
>
{/* Content */}
</DialogPanel>
</Dialog>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