Browse the docs
Primitives
The behavior layer under the richer components: headless, unstyled building
blocks in the spirit of Radix UI. A primitive renders no Tailwind classes —
it provides state, keyboard handling, ARIA wiring, positioning, and exposes
data-state / data-side / data-highlighted attributes for your CSS.
Two ways to install, same source either way:
Copy a file. Every primitive downloads as a single file with the shared
core embedded — save it under primitives/ and import it relatively:
curl -o primitives/popover.js https://presetui.com/r/primitives/popover.txtCopying several? Fetch /r/primitives/core.txt
once instead, save it as primitives/core.js, and use each primitive's
repo form (which imports ./core.js) from the Source views on this site.
Install a package. Using the full kit? The primitives are already inside
@presetui/ui:
import { Popover, PopoverContent, PopoverTrigger } from "@presetui/ui/primitives/popover"Behavior only — no Tailwind, no styled layer? The standalone package is the same source:
npm install @presetui/primitivesimport { Popover, PopoverContent, PopoverTrigger } from "@presetui/primitives/popover"Both ship subpath exports per primitive and .d.ts types generated from the
JSDoc, with a React 19+ peer as the only requirement. Styled components that
build on a primitive show a Source · npm tab with their import already
pointing at the bundled copy (@presetui/ui/primitives/<name>) — one
install, nothing else to add.
The contract
- Compound components:
Popover/PopoverTrigger/PopoverContent, not a props bag — compose them around your own markup. - Zero styling. Style via the data attributes:
data-state="open|closed",data-side,data-highlighted,data-disabled. - Every part forwards
className,style,ref, and all DOM props. - Triggers accept
asChildto merge their behavior onto your element instead of rendering a<button>. - Each file's head comment documents its parts, ARIA, keyboard map, and data attributes in full — one file is the whole story.
- Imports:
reactand./core.js. Nothing else, ever (npm run checkenforces it).
The primitives
| Primitive | Parts | Used by |
|---|---|---|
accordion | AccordionRoot, AccordionItem, AccordionTrigger, AccordionContent | Accordion |
tabs | TabsRoot, TabsList, TabsTrigger, TabsContent | Tabs |
popover | Popover, PopoverTrigger, PopoverAnchor, PopoverContent, PopoverClose | Popover |
dropdown-menu | Menu, MenuTrigger, MenuContent, MenuItem, MenuGroup, MenuLabel, MenuSeparator | Dropdown Menu |
listbox | SelectRoot, SelectTrigger, SelectValue, SelectContent, SelectOption, SelectGroup | Select Menu |
combobox | Combobox, ComboboxInput, ComboboxContent, ComboboxOption, ComboboxEmpty | Combobox |
tooltip | TooltipRoot, TooltipTrigger, TooltipContent | Tooltip (JS) |
dialog | DialogRoot, DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogClose | Sheet |
toast | createToaster, toast, useToasts, Toaster | Toast |
core | useControllableState, usePosition, useDismiss, useRovingFocus, useTypeahead, focusFirst, composeRefs, mergeProps | all of the above |
Where the native-first components stand
Select stays a native <select>, Tooltip
stays CSS-only, Dialog stays a single file — each with a
primitive-based alternative when you outgrow the trade-off
(Select Menu, Tooltip (JS),
Sheet). Accordion still renders native
<details>/<summary> markup — its primitive just moves the open state
into React, which is what makes exclusive groups work in every browser.
The primitives exist for the interactions the platform doesn't provide, and
for composition the data-prop components don't allow.