Headless UI Library
A production-quality, headless, accessible React component library built with TypeScript. This library provides unstyled, accessible UI primitives that you can style however you want.
What is Headless UI?
Headless UI components provide all the functionality, accessibility, and behavior of a component without any styling. This gives you complete control over the appearance while ensuring:
- Accessibility: WAI-ARIA compliant components
- Flexibility: Style with any CSS solution (CSS-in-JS, Tailwind, CSS Modules, etc.)
- Composability: Build complex UIs from simple primitives
- Type Safety: Full TypeScript support
Installation
Since this library is part of your project, import components directly:
import { Dialog } from '@/lib/ui';
Components
Dialog
A fully accessible modal dialog component with focus management and keyboard navigation.
Features
- ✅ Controlled and uncontrolled modes
- ✅ Focus trap with automatic focus management
- ✅ Keyboard support (Escape to close, Tab cycling)
- ✅ Click outside to close
- ✅ Body scroll lock when open
- ✅ Focus return to trigger on close
- ✅ WAI-ARIA compliant
- ✅ Compound component pattern
- ✅
asChild pattern for flexible composition
Basic Usage
import { Dialog } from '@/lib/ui';
function MyDialog() {
return (
<Dialog>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }} />
<Dialog.Content
style={{
backgroundColor: 'white',
padding: '2rem',
borderRadius: '8px',
minWidth: '400px',
}}
>
<Dialog.Title style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
Confirm Action
</Dialog.Title>
<Dialog.Description style={{ marginTop: '0.5rem', color: '#666' }}>
Are you sure you want to proceed? This action cannot be undone.
</Dialog.Description>
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '0.5rem' }}>
<Dialog.Close style={{ padding: '0.5rem 1rem' }}>
Cancel
</Dialog.Close>
<button style={{ padding: '0.5rem 1rem' }}>
Confirm
</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
}
Controlled Mode
import { useState } from 'react';
import { Dialog } from '@/lib/ui';
function ControlledDialog() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open Dialog</button>
<Dialog open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }} />
<Dialog.Content style={{ backgroundColor: 'white', padding: '2rem' }}>
<Dialog.Title>Controlled Dialog</Dialog.Title>
<Dialog.Description>
This dialog's state is controlled externally.
</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
</>
);
}
Using asChild Pattern
The asChild pattern allows you to use your own custom elements while preserving Dialog functionality:
import { Dialog } from '@/lib/ui';
function CustomDialog() {
return (
<Dialog>
{/* Use your own button component */}
<Dialog.Trigger asChild>
<button className="custom-button">
Open Dialog
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="custom-overlay" />
<Dialog.Content className="custom-content">
<Dialog.Title className="custom-title">Title</Dialog.Title>
<Dialog.Description>Description</Dialog.Description>
{/* Use your own close button */}
<Dialog.Close asChild>
<button className="custom-close-button">
Close
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
}
API Reference
<Dialog>
Root component that provides context for all sub-components.
open | boolean | - | Controlled open state |
defaultOpen | boolean | false | Default open state (uncontrolled) |
onOpenChange | (open: boolean) => void | - | Callback when open state changes |
<Dialog.Trigger>
Button that opens the dialog.
asChild | boolean | false | Render as child element instead of button |
| ...buttonProps | - | - | All standard button HTML attributes |
<Dialog.Portal>
Renders children in a portal outside the DOM hierarchy.
container | HTMLElement | document.body | Portal container element |
<Dialog.Overlay>
Semi-transparent backdrop. Clicking it closes the dialog.
| ...divProps | - | - | All standard div HTML attributes |
<Dialog.Content>
Main dialog content container.
onEscapeKeyDown | (event: KeyboardEvent) => void | - | Callback when Escape is pressed |
onOverlayClick | (event: MouseEvent) => void | - | Callback when overlay is clicked |
| ...divProps | - | - | All standard div HTML attributes |
<Dialog.Title>
Dialog title, linked via aria-labelledby.
| ...h2Props | - | - | All standard h2 HTML attributes |
<Dialog.Description>
Dialog description, linked via aria-describedby.
| ...pProps | - | - | All standard p HTML attributes |
<Dialog.Close>
Button that closes the dialog.
asChild | boolean | false | Render as child element instead of button |
| ...buttonProps | - | - | All standard button HTML attributes |
Accessibility
ARIA Attributes
The Dialog component automatically manages all required ARIA attributes:
role="dialog" on content
aria-modal="true" on content
aria-labelledby linking to title
aria-describedby linking to description
Focus Management
- Focus Trap: Focus is trapped inside the dialog when open
- Auto Focus: First focusable element is focused when dialog opens
- Focus Return: Focus returns to trigger button when dialog closes
- Tab Cycling: Tab and Shift+Tab cycle through focusable elements
Keyboard Support
Escape | Closes the dialog |
Tab | Moves focus to next focusable element (cycles at end) |
Shift + Tab | Moves focus to previous focusable element (cycles at start) |
Screen Reader Support
- Dialog is announced as a modal dialog
- Title is announced as the dialog label
- Description provides additional context
- All interactive elements are keyboard accessible
Architecture
Compound Components
The Dialog uses the compound component pattern, where sub-components share state through React Context. This provides:
- Type Safety: TypeScript ensures correct usage
- Clear API: Explicit component hierarchy
- Flexibility: Compose components as needed
Hooks
The library exposes several hooks for advanced usage:
useControlled: Manage controlled/uncontrolled state
useFocusTrap: Trap focus within a container
useEscapeKeydown: Handle Escape key events
useBodyScrollLock: Prevent body scroll
useStableId: Generate stable unique IDs
Slot Pattern
The asChild pattern is implemented using the Slot component, which:
- Merges props from the Dialog component with your custom element
- Composes event handlers
- Forwards refs correctly
- Preserves TypeScript types
Extending the Library
This library is designed to be extended with more headless components. When adding new components:
- Follow the same patterns: Use compound components, context, and hooks
- Prioritize accessibility: Follow WAI-ARIA guidelines
- Support controlled/uncontrolled: Use the
useControlled hook
- Implement
asChild: Use the Slot component for flexibility
- Type everything: No
any types in public APIs
- Document thoroughly: Include examples and API reference
Recommended Components to Add
- Menu: Dropdown menus with keyboard navigation
- Tabs: Accessible tab panels
- Popover: Floating content containers
- Tooltip: Accessible tooltips
- Accordion: Collapsible content sections
- Select: Custom select dropdowns
- Combobox: Autocomplete inputs
License
This library is part of your project and follows your project's license.