A set of completely unstyled, fully accessible UI components for React, designed to integrate beautifully with Tailwind CSS.
84
Supporting components and utilities including focus management, portals, labels, descriptions, transitions, and interactive state helpers.
A component that traps focus within its boundaries, essential for modal dialogs and other overlay components.
/**
* Component that traps focus within its boundaries
* @param props - FocusTrap properties including feature configuration
*/
function FocusTrap<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: FocusTrapProps<TTag>
): JSX.Element;
interface FocusTrapProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Initial focus target element */
initialFocus?: React.MutableRefObject<HTMLElement | null>;
/** Fallback focus target if initial focus fails */
initialFocusFallback?: React.MutableRefObject<HTMLElement | null>;
/** Feature flags controlling focus trap behavior */
features?: FocusTrapFeatures;
/** Additional containers to include in focus trap */
containers?: Containers;
}
enum FocusTrapFeatures {
/** No special features */
None = 0,
/** Move focus to initial element on mount */
InitialFocus = 1,
/** Trap tab navigation within boundaries */
TabLock = 2,
/** Lock programmatic focus changes */
FocusLock = 4,
/** Restore focus to previous element on unmount */
RestoreFocus = 8,
/** Look for data-autofocus attribute */
AutoFocus = 16
}
type Containers = React.MutableRefObject<Set<React.MutableRefObject<HTMLElement | null>>>;Usage Examples:
import { FocusTrap, FocusTrapFeatures } from "@headlessui/react";
import { useRef } from "react";
function FocusTrapExample() {
const initialFocusRef = useRef(null);
return (
<FocusTrap
initialFocus={initialFocusRef}
features={
FocusTrapFeatures.InitialFocus |
FocusTrapFeatures.TabLock |
FocusTrapFeatures.RestoreFocus
}
className="bg-white p-6 rounded-lg shadow-lg"
>
<h2 className="text-lg font-semibold mb-4">Focus Trapped Content</h2>
<input
ref={initialFocusRef}
placeholder="This gets initial focus"
className="w-full p-2 border border-gray-300 rounded mb-4"
/>
<button className="px-4 py-2 bg-blue-500 text-white rounded mr-2">
Button 1
</button>
<button className="px-4 py-2 bg-green-500 text-white rounded">
Button 2
</button>
{/* Focus will cycle between these elements only */}
</FocusTrap>
);
}
// Advanced focus trap with multiple containers
function AdvancedFocusTrapExample() {
const containersRef = useRef(new Set());
const sidebarRef = useRef(null);
const mainRef = useRef(null);
// Add containers to the set
React.useEffect(() => {
containersRef.current.add(sidebarRef);
containersRef.current.add(mainRef);
}, []);
return (
<FocusTrap
containers={containersRef}
features={FocusTrapFeatures.TabLock | FocusTrapFeatures.RestoreFocus}
>
<div className="flex">
<div ref={sidebarRef} className="w-64 bg-gray-100 p-4">
<button>Sidebar Button</button>
</div>
<div ref={mainRef} className="flex-1 p-4">
<button>Main Button</button>
</div>
</div>
</FocusTrap>
);
}A portal component for rendering content outside the normal DOM tree to avoid z-index and overflow issues.
/**
* Portal component for rendering outside normal DOM tree
* @param props - Portal properties including target document
*/
function Portal(props: PortalProps): JSX.Element;
interface PortalProps {
/** Whether portal is enabled */
enabled?: boolean;
/** Target document for portal rendering */
ownerDocument?: Document | null;
/** Content to render in portal */
children: React.ReactNode;
}Groups multiple portals together for coordinated behavior.
/**
* Groups multiple portals together
* @param props - PortalGroup properties
*/
function PortalGroup(props: PortalGroupProps): JSX.Element;
interface PortalGroupProps {
/** Target element for grouped portals */
target?: HTMLElement;
/** Content containing portals */
children: React.ReactNode;
}Hook for managing nested portal relationships.
/**
* Hook for managing nested portals
* @returns Portal management utilities
*/
function useNestedPortals(): {
/** Register a nested portal */
registerPortal: (element: HTMLElement) => void;
/** Unregister a nested portal */
unregisterPortal: (element: HTMLElement) => void;
/** Check if element is within a nested portal */
isWithinPortal: (element: HTMLElement) => boolean;
};Usage Examples:
import { Portal, PortalGroup } from "@headlessui/react";
import { useState } from "react";
function PortalExample() {
const [showTooltip, setShowTooltip] = useState(false);
return (
<div className="p-4">
<button
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Hover me
</button>
{/* This renders at document body level */}
<Portal enabled={showTooltip}>
<div className="absolute top-20 left-20 bg-black text-white p-2 rounded shadow-lg">
This tooltip is rendered in a portal!
</div>
</Portal>
</div>
);
}
// Conditional portal usage
function ConditionalPortalExample() {
const [usePortal, setUsePortal] = useState(true);
const content = (
<div className="bg-red-100 p-4 rounded">
This content may or may not be in a portal
</div>
);
return (
<div>
<label className="flex items-center mb-4">
<input
type="checkbox"
checked={usePortal}
onChange={(e) => setUsePortal(e.target.checked)}
className="mr-2"
/>
Use Portal
</label>
{usePortal ? (
<Portal>{content}</Portal>
) : (
content
)}
</div>
);
}
// Portal group example
function PortalGroupExample() {
return (
<PortalGroup>
<div className="space-y-4">
<Portal>
<div className="fixed top-4 right-4 bg-green-500 text-white p-2 rounded">
Notification 1
</div>
</Portal>
<Portal>
<div className="fixed top-16 right-4 bg-blue-500 text-white p-2 rounded">
Notification 2
</div>
</Portal>
</div>
</PortalGroup>
);
}A label component that automatically associates with form controls and provides proper accessibility.
/**
* Label component with automatic form control association
* @param props - Label properties including passive mode
*/
function Label<TTag extends keyof JSX.IntrinsicElements = 'label'>(
props: LabelProps<TTag>
): JSX.Element;
interface LabelProps<TTag extends keyof JSX.IntrinsicElements = 'label'>
extends PolymorphicProps<TTag> {
/** Whether to disable click behavior (passive mode) */
passive?: boolean;
/** Explicit control ID to associate with */
htmlFor?: string;
}Hook to get label IDs for aria-labelledby attribute.
/**
* Hook to get label IDs for aria-labelledby
* @returns Comma-separated string of label IDs
*/
function useLabelledBy(): string | undefined;Hook for managing label associations.
/**
* Hook for managing label associations
* @returns Label management utilities
*/
function useLabels(): {
/** Register a label */
register: (id: string) => void;
/** Unregister a label */
unregister: (id: string) => void;
/** Get all label IDs */
labelIds: string[];
};Usage Examples:
import { Label, Field, Input, useLabelledBy } from "@headlessui/react";
function LabelExample() {
return (
<Field className="space-y-2">
<Label className="block text-sm font-medium text-gray-700">
Email Address
</Label>
<Input
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
/>
</Field>
);
}
// Multiple labels for one control
function MultipleLabelExample() {
return (
<Field>
<div className="space-y-1">
<Label className="block text-sm font-medium text-gray-700">
Password
</Label>
<Label passive className="block text-xs text-gray-500">
Must be at least 8 characters
</Label>
</div>
<Input
type="password"
className="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md"
/>
</Field>
);
}
// Using label hooks
function CustomControlWithLabel() {
const labelIds = useLabelledBy();
return (
<div>
<Label>Custom Control</Label>
<Label passive>Additional context</Label>
<div
role="slider"
aria-labelledby={labelIds}
tabIndex={0}
className="w-32 h-6 bg-gray-200 rounded-full cursor-pointer"
>
{/* Custom slider implementation */}
</div>
</div>
);
}A description component that automatically associates with form controls for accessibility.
/**
* Description component with automatic form control association
* @param props - Description properties
*/
function Description<TTag extends keyof JSX.IntrinsicElements = 'p'>(
props: DescriptionProps<TTag>
): JSX.Element;
interface DescriptionProps<TTag extends keyof JSX.IntrinsicElements = 'p'>
extends PolymorphicProps<TTag> {
/** Standard description properties */
}Hook to get description ID for aria-describedby attribute.
/**
* Hook to get description ID for aria-describedby
* @returns Description ID string
*/
function useDescribedBy(): string | undefined;Usage Examples:
import { Description, Field, Label, Input, useDescribedBy } from "@headlessui/react";
function DescriptionExample() {
return (
<Field className="space-y-2">
<Label className="block text-sm font-medium text-gray-700">
Username
</Label>
<Input
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="Enter username"
/>
<Description className="text-sm text-gray-500">
Username must be unique and contain only letters, numbers, and underscores.
</Description>
</Field>
);
}
// Multiple descriptions
function MultipleDescriptionExample() {
return (
<Field>
<Label>Password</Label>
<Input type="password" />
<Description className="text-sm text-gray-600">
Password requirements:
</Description>
<Description className="text-xs text-gray-500 ml-4">
• At least 8 characters long
</Description>
<Description className="text-xs text-gray-500 ml-4">
• Contains uppercase and lowercase letters
</Description>
<Description className="text-xs text-gray-500 ml-4">
• Contains at least one number
</Description>
</Field>
);
}
// Using description hook
function CustomControlWithDescription() {
const descriptionId = useDescribedBy();
return (
<div>
<label>Custom Slider</label>
<Description>Adjust the value between 0 and 100</Description>
<div
role="slider"
aria-describedby={descriptionId}
tabIndex={0}
className="w-64 h-6 bg-gray-200 rounded-full"
>
{/* Custom slider implementation */}
</div>
</div>
);
}A component for managing CSS transitions with declarative syntax.
/**
* Component for managing CSS transitions
* @param props - Transition properties including CSS classes and timing
*/
function Transition<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: TransitionProps<TTag>
): JSX.Element;
interface TransitionProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Whether to show the content (triggers transition) */
show?: boolean;
/** Whether to animate on initial render */
appear?: boolean;
/** CSS classes for enter transition */
enter?: string;
/** CSS classes for enter start state */
enterFrom?: string;
/** CSS classes for enter end state */
enterTo?: string;
/** CSS classes for leave transition */
leave?: string;
/** CSS classes for leave start state */
leaveFrom?: string;
/** CSS classes for leave end state */
leaveTo?: string;
/** Callback before enter transition starts */
beforeEnter?: () => void;
/** Callback after enter transition completes */
afterEnter?: () => void;
/** Callback before leave transition starts */
beforeLeave?: () => void;
/** Callback after leave transition completes */
afterLeave?: () => void;
}Individual transition child component for complex transitions.
/**
* Individual transition child component
* @param props - TransitionChild properties
*/
function TransitionChild<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: TransitionChildProps<TTag>
): JSX.Element;
interface TransitionChildProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** CSS classes for enter transition */
enter?: string;
/** CSS classes for enter start state */
enterFrom?: string;
/** CSS classes for enter end state */
enterTo?: string;
/** CSS classes for leave transition */
leave?: string;
/** CSS classes for leave start state */
leaveFrom?: string;
/** CSS classes for leave end state */
leaveTo?: string;
/** Callback before enter transition starts */
beforeEnter?: () => void;
/** Callback after enter transition completes */
afterEnter?: () => void;
/** Callback before leave transition starts */
beforeLeave?: () => void;
/** Callback after leave transition completes */
afterLeave?: () => void;
}Usage Examples:
import { useState } from "react";
import { Transition, TransitionChild, Button } from "@headlessui/react";
function BasicTransitionExample() {
const [show, setShow] = useState(false);
return (
<div>
<Button onClick={() => setShow(!show)}>
Toggle
</Button>
<Transition
show={show}
enter="transition-opacity duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="bg-blue-500 text-white p-4 rounded-lg mt-4">
This content fades in and out
</div>
</Transition>
</div>
);
}
// Complex transition with multiple children
function ComplexTransitionExample() {
const [show, setShow] = useState(false);
return (
<div>
<Button onClick={() => setShow(!show)}>
Toggle Modal
</Button>
<Transition show={show}>
{/* Backdrop */}
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
className="fixed inset-0 bg-black bg-opacity-25"
/>
{/* Modal panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className="bg-white rounded-lg p-6 shadow-xl max-w-md w-full"
>
<h3 className="text-lg font-medium mb-4">Modal Title</h3>
<p className="text-gray-600 mb-4">Modal content goes here...</p>
<Button onClick={() => setShow(false)}>
Close
</Button>
</TransitionChild>
</div>
</Transition>
</div>
);
}
// Transition with callbacks
function TransitionWithCallbacksExample() {
const [show, setShow] = useState(false);
return (
<div>
<Button onClick={() => setShow(!show)}>
Toggle with Callbacks
</Button>
<Transition
show={show}
beforeEnter={() => console.log('About to enter')}
afterEnter={() => console.log('Enter complete')}
beforeLeave={() => console.log('About to leave')}
afterLeave={() => console.log('Leave complete')}
enter="transition-all duration-500"
enterFrom="opacity-0 transform scale-50"
enterTo="opacity-100 transform scale-100"
leave="transition-all duration-300"
leaveFrom="opacity-100 transform scale-100"
leaveTo="opacity-0 transform scale-50"
className="bg-green-500 text-white p-4 rounded-lg mt-4 origin-center"
>
Content with transition callbacks
</Transition>
</div>
);
}A wrapper component that provides interaction states without default behavior, useful for custom interactive elements.
/**
* Wrapper providing interaction states without default behavior
* @param props - DataInteractive properties
*/
function DataInteractive<TTag extends keyof JSX.IntrinsicElements = 'div'>(
props: DataInteractiveProps<TTag>
): JSX.Element;
interface DataInteractiveProps<TTag extends keyof JSX.IntrinsicElements = 'div'>
extends PolymorphicProps<TTag> {
/** Render prop providing interaction state */
children?: React.ReactNode | ((props: DataInteractiveRenderProps) => React.ReactNode);
}
interface DataInteractiveRenderProps {
/** Whether being hovered */
hover: boolean;
/** Whether has focus */
focus: boolean;
/** Whether being pressed */
active: boolean;
}Usage Examples:
import { DataInteractive } from "@headlessui/react";
function DataInteractiveExample() {
return (
<DataInteractive as="div" tabIndex={0}>
{({ hover, focus, active }) => (
<div
className={`
p-4 rounded-lg cursor-pointer transition-colors
${hover ? 'bg-blue-100' : 'bg-gray-100'}
${focus ? 'ring-2 ring-blue-500' : ''}
${active ? 'bg-blue-200' : ''}
`}
>
Custom interactive element
<div className="text-sm text-gray-600 mt-2">
Hover: {hover ? '✓' : '✗'} |
Focus: {focus ? '✓' : '✗'} |
Active: {active ? '✓' : '✗'}
</div>
</div>
)}
</DataInteractive>
);
}
// Custom card with interactive states
function InteractiveCard() {
return (
<DataInteractive
as="article"
tabIndex={0}
onClick={() => console.log('Card clicked')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
console.log('Card activated');
}
}}
>
{({ hover, focus, active }) => (
<div
className={`
border rounded-lg p-6 transition-all duration-200
${hover ? 'shadow-lg border-blue-300' : 'shadow border-gray-200'}
${focus ? 'ring-2 ring-blue-500 ring-offset-2' : ''}
${active ? 'scale-95' : ''}
`}
>
<h3 className="text-lg font-semibold mb-2">Interactive Card</h3>
<p className="text-gray-600">
This card responds to hover, focus, and active states.
</p>
</div>
)}
</DataInteractive>
);
}A hook that provides access to the close function from the nearest overlay component context.
/**
* Hook providing access to close function from component context
* @returns Function to close the containing component
*/
function useClose(): () => void;Usage Examples:
import { Dialog, DialogPanel, useClose } from "@headlessui/react";
function DialogContent() {
const close = useClose();
return (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Dialog Content</h2>
<p>This component can close the dialog using the useClose hook.</p>
<div className="flex gap-2">
<button
onClick={close}
className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600"
>
Cancel
</button>
<button
onClick={() => {
// Do something then close
console.log('Action completed');
close();
}}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Confirm
</button>
</div>
</div>
);
}
function DialogWithUseClose() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open Dialog</button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel className="bg-white p-6 rounded-lg">
<DialogContent />
</DialogPanel>
</div>
</Dialog>
</>
);
}
// Custom hook using useClose
function useDialogAction() {
const close = useClose();
const handleSave = async (data: any) => {
try {
await saveData(data);
close(); // Close dialog on successful save
} catch (error) {
console.error('Save failed:', error);
// Don't close on error
}
};
return { handleSave };
}// Base polymorphic props for all utility components
interface PolymorphicProps<TTag extends keyof JSX.IntrinsicElements = 'div'> {
/** Element or component to render as */
as?: TTag;
/** Children content or render prop function */
children?: React.ReactNode | ((props: any) => React.ReactNode);
}
// Focus trap feature flags
enum FocusTrapFeatures {
None = 0,
InitialFocus = 1,
TabLock = 2,
FocusLock = 4,
RestoreFocus = 8,
AutoFocus = 16
}
// Container type for focus trap
type Containers = React.MutableRefObject<Set<React.MutableRefObject<HTMLElement | null>>>;
// Common interaction state
interface InteractionState {
hover: boolean;
focus: boolean;
active: boolean;
}
// Transition callback functions
type TransitionCallback = () => void;Utility components enhance accessibility in several ways:
These utilities are designed to work together and with other Headless UI components:
// Common pattern: Dialog with all utilities
<Dialog open={isOpen} onClose={setIsOpen}>
<Portal>
<Transition show={isOpen}>
<FocusTrap>
<DialogPanel>
<Label>Dialog Title</Label>
<Description>Dialog description</Description>
{/* Content */}
</DialogPanel>
</FocusTrap>
</Transition>
</Portal>
</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