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>