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

PrimitiveDescriptionUse Case
AccordionCollapsible content sectionsMulti-section expandable content
AlertDialogConfirmation modal dialogCritical confirmations (delete, logout)
ArrowFloating arrow for popovers/tooltipsVisual pointer for floating content
AvatarUser avatar with image and fallbackUser profile pictures, team members
CheckboxUnstyled checkbox inputMultiple selections, toggleable lists
CollapsibleExpandable/collapsible panelToggle visibility of content sections
CommandKeyboard-navigable command listCommand palettes, search results
DialogModal dialog containerGeneral modals, custom dialogs
DropdownMenuContext menu / action menuRight-click menus, action dropdowns
FocusScopeFocus trap and managementModal focus containment, focus isolation
LabelAccessible form labelForm field labels
MenuAccessible menu systemNavigation menus, action menus
PopoverFloating popover anchored to a triggerPopovers, info panels
PopperLow-level floating UI primitiveFoundation for popovers/tooltips
PortalRender content outside the DOM treeModals, tooltips, floating content
PresenceAnimation-aware mount/unmount stateExit animations, fade-out timing
PrimitiveUnstyled base element (all HTML tags)Building custom components
RadioGroupRadio button groupSingle selection from multiple options
RovingFocusArrow-key focus navigationLists, menus, tabs (via arrow keys)
SelectAccessible select dropdownCustom select dropdowns with search
SeparatorVisual dividerContent sections, navigation dividers
SlotComponent composition via asChildProp merging, composition abstraction
SwitchToggle switch inputOn/off toggles, boolean settings
TabsTabbed content interfaceMulti-panel content organization
TooltipHover tooltipHelper text, keyboard shortcuts
VisuallyHiddenScreen-reader-only contentAccessible 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 value and onChange for controlled mode (parent owns state)
  • Pass defaultValue for 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 asChild on 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