Primitives
Unstyled, accessible headless components from @loke/ui for building custom-styled UI.
Primitives
@loke/ui exports 26 unstyled, accessible headless components that form the foundation of @loke/design-system. These primitives provide behavior and accessibility without opinions about appearance. Use them directly when you need full styling control or want to build custom components with predictable, accessible interactions.
Design System vs Primitives: Use @loke/design-system for pre-styled, ready-to-use components. Use @loke/ui primitives when you need full control over styling or want to build specialized component variants.
When to Use Primitives
Choose primitives when:
- You need custom styling that differs from the design system
- Building specialized components for a specific domain (e.g., rich text editor controls)
- Creating variants that aren't in the design system
- Avoiding design-system dependencies in a utility library
- You want only the behavior and accessibility, not the visual design
Always prefer design-system components for standard UI (buttons, inputs, modals, etc.) unless you have a specific reason to customize.
All 26 Primitives
| Primitive | Description | Use Case |
|---|---|---|
| Accordion | Collapsible content sections | Multi-section expandable content |
| AlertDialog | Confirmation modal dialog | Critical confirmations (delete, logout) |
| Arrow | Floating arrow for popovers/tooltips | Visual pointer for floating content |
| Avatar | User avatar with image and fallback | User profile pictures, team members |
| Checkbox | Unstyled checkbox input | Multiple selections, toggleable lists |
| Collapsible | Expandable/collapsible panel | Toggle visibility of content sections |
| Command | Keyboard-navigable command list | Command palettes, search results |
| Dialog | Modal dialog container | General modals, custom dialogs |
| DropdownMenu | Context menu / action menu | Right-click menus, action dropdowns |
| FocusScope | Focus trap and management | Modal focus containment, focus isolation |
| Label | Accessible form label | Form field labels |
| Menu | Accessible menu system | Navigation menus, action menus |
| Popover | Floating popover anchored to a trigger | Popovers, info panels |
| Popper | Low-level floating UI primitive | Foundation for popovers/tooltips |
| Portal | Render content outside the DOM tree | Modals, tooltips, floating content |
| Presence | Animation-aware mount/unmount state | Exit animations, fade-out timing |
| Primitive | Unstyled base element (all HTML tags) | Building custom components |
| RadioGroup | Radio button group | Single selection from multiple options |
| RovingFocus | Arrow-key focus navigation | Lists, menus, tabs (via arrow keys) |
| Select | Accessible select dropdown | Custom select dropdowns with search |
| Separator | Visual divider | Content sections, navigation dividers |
| Slot | Component composition via asChild | Prop merging, composition abstraction |
| Switch | Toggle switch input | On/off toggles, boolean settings |
| Tabs | Tabbed content interface | Multi-panel content organization |
| Tooltip | Hover tooltip | Helper text, keyboard shortcuts |
| VisuallyHidden | Screen-reader-only content | Accessible labels, hidden context |
Core Patterns
Most primitives follow these patterns:
Composed Components: Root + sub-components
- Root provides state and context
- Sub-components render UI and consume context
- Example:
<Dialog>+<DialogTrigger>+<DialogContent>
Controlled & Uncontrolled: Flexible state management
- Pass
valueandonChangefor controlled mode (parent owns state) - Pass
defaultValuefor uncontrolled mode (component owns state) - Example:
<Switch checked={true} onCheckedChange={...} />or<Switch defaultChecked />
Headless: Styling is your responsibility
- Components render semantic HTML (buttons, divs, etc.)
- Props are forwarded (className, style, data-*)
- Use
asChildon Primitive to merge props onto custom elements - Style with Tailwind, CSS-in-JS, or any styling approach
Import Paths
Import from granular entry points:
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@loke/ui/accordion';
import { Dialog, DialogTrigger, DialogContent } from '@loke/ui/dialog';
import { Switch, SwitchThumb } from '@loke/ui/switch';
import { useControllableState } from '@loke/ui/use-controllable-state';
import { Portal } from '@loke/ui/portal';Never import from the package root; use specific subpaths for tree-shaking.
Accessibility First
Every primitive follows WAI-ARIA patterns:
- Correct semantic roles (
button,dialog,tab, etc.) - Full keyboard support (Tab, arrow keys, Enter, Escape)
- Screen reader annotations (aria-label, aria-expanded, aria-checked, etc.)
- Focus management (traps in modals, roving tab index in lists)
- Tested with NVDA, JAWS, and VoiceOver
Common Tasks
Build a custom modal: Use Dialog + FocusScope + Portal
Build a custom select: Use Select or Popper + Command + RovingFocus
Build a custom form: Combine Checkbox, RadioGroup, Switch, and Label
Animate content: Use Presence to delay unmounting, Portal for layering
Add tooltip to button: Wrap Tooltip around TooltipTrigger + TooltipContent
TypeScript Support
All primitives are fully typed with TypeScript. Component props extend native HTML attributes and include custom props for behavior:
type SwitchProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
disabled?: boolean;
required?: boolean;
};Browser Support
Primitives work in all modern browsers (Chrome, Firefox, Safari, Edge). React 16.8+ required (hooks support).
Where to start
- Building a modal or dialog? Use Dialog + FocusScope + Portal
- Need a dropdown or popover? Try Popover or DropdownMenu — both handle positioning and dismissal
- Building a custom form input? Explore Checkbox, Switch, or RadioGroup
- Complex accordion or tabs? Start with Accordion or Tabs
- Need a menu system? Try Menu or Command for keyboard-navigable lists
- Styling your own components? Use Primitive as a base element with
asChildfor composition