@robonen/primitives
A collection of unstyled, accessible UI primitives for Vue 3 — the headless building blocks for design systems and component libraries.
Most component libraries bundle behavior and styling together, so the moment your design diverges you end up fighting the framework. @robonen/primitives ships the hard part — state, focus management, keyboard interaction, ARIA wiring, portalling and positioning — and leaves the markup and styling entirely to you. Every primitive is composed from small, controllable parts (a Root, a Trigger, a Content, and so on) following the same conventions, so once you learn one you know them all.
Unstyled by design
No CSS shipped. Primitives render the DOM you ask for and expose state via data attributes, so you bring your own styles — Tailwind, vanilla CSS, anything.
Accessible out of the box
Focus scopes, roving tabindex, visually-hidden labels and correct ARIA roles are handled for you. The suite is tested against axe-core in a real browser.
Controlled or uncontrolled
Bind state with v-model when you need control, or set a defaultValue / defaultOpen and let the primitive manage itself.
Composable & polymorphic
Every part takes an as prop, or use as="template" to merge behavior onto your own element. Floating UI powers positioning for popovers, tooltips and menus.
Install
pnpm add @robonen/primitivesUsage
Primitives are assembled from named parts. Here is a complete dialog — open state is uncontrolled, focus is trapped, body scroll is locked, and the content is portalled out of the DOM flow:
<script setup lang="ts">
import {
DialogRoot,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from '@robonen/primitives';
</script>
<template>
<DialogRoot>
<DialogTrigger class="btn">Open</DialogTrigger>
<DialogPortal>
<DialogOverlay class="overlay" />
<DialogContent class="dialog">
<DialogTitle>Delete project</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
<DialogClose class="btn">Cancel</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>Need full control over open state? Bind it directly — the same primitive works either way:
<DialogRoot v-model:open="isOpen">
<!-- ... -->
</DialogRoot>The Primitive component
At the core of every part is Primitive, a polymorphic functional component. Pass as to choose the element, or as="template" to forward behavior onto a child of your own.
import { Primitive, Slot } from '@robonen/primitives';
// <Primitive as="button" /> renders a <button>
// <Primitive as="template"> merges props onto the slotted childWhere to next
The full primitive index is listed below. A few good starting points:
- Dialog and Alert Dialog — modal layers with focus trapping.
- Popover, Tooltip and Hover Card — Floating UI positioned surfaces.
- Select, Combobox and Listbox — keyboard-driven option pickers.
- Switch, Checkbox and Slider — form controls that integrate with native inputs.
- Focus Scope and Presence — the shared foundations every part builds on.