From pwdev-uiux
Skill for direct usage of Reka UI v2 headless primitives (the foundation of shadcn-vue). Use when you need to compose custom components beyond those available in shadcn-vue, understand asChild/as, controlled state, portals, or migrate from Radix Vue.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pwdev-uiux:reka-uiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Reka UI is the successor to Radix Vue. Foundation of all shadcn-vue@latest components.
Reka UI is the successor to Radix Vue. Foundation of all shadcn-vue@latest components. Current version: v2.7.0 (March 2026)
# Via shadcn-vue (recommended — includes styles)
npx shadcn-vue@latest add [component]
# Direct (for unstyled primitives)
npm install reka-ui
# Nuxt — auto-imports
# nuxt.config.ts
export default defineNuxtConfig({
modules: ['reka-ui/nuxt']
})
# Vite — auto-import resolver
# vite.config.ts
import { RekaResolver } from 'reka-ui/resolver'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
vue(),
Components({ resolvers: [RekaResolver()] })
]
})
Every Reka UI component follows the Root + sub-parts pattern:
<script setup lang="ts">
import {
AccordionRoot,
AccordionItem,
AccordionHeader,
AccordionTrigger,
AccordionContent,
} from 'reka-ui'
const value = ref(['item-1'])
</script>
<template>
<!-- Root: manages state -->
<AccordionRoot v-model="value" type="multiple">
<!-- Item: context for each entry -->
<AccordionItem value="item-1">
<!-- Header + Trigger: accessible clickable element -->
<AccordionHeader>
<AccordionTrigger>Title</AccordionTrigger>
</AccordionHeader>
<!-- Content: expandable panel -->
<AccordionContent>Item content</AccordionContent>
</AccordionItem>
</AccordionRoot>
</template>
<!-- Controlled: you control the state -->
<DialogRoot v-model:open="isOpen">
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogContent>...</DialogContent>
</DialogPortal>
</DialogRoot>
<!-- Uncontrolled: Reka UI manages internally -->
<DialogRoot :default-open="false">
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogContent>...</DialogContent>
</DialogPortal>
</DialogRoot>
Convention: default* prefix for initial values (uncontrolled).
v-model:open, v-model:value, v-model:checked for controlled.
asChild allows the Reka UI component to apply its behavior to the child element:
<!-- WITHOUT asChild: Reka renders an extra <button> -->
<DialogTrigger>
<Button>Open</Button> <!-- Button inside button = invalid HTML -->
</DialogTrigger>
<!-- WITH asChild: Reka applies handlers to Button directly -->
<DialogTrigger as-child>
<Button>Open</Button> <!-- Correct: only one interactive element -->
</DialogTrigger>
<!-- Navigation with RouterLink -->
<NavigationMenuLink as-child>
<RouterLink to="/dashboard">Dashboard</RouterLink>
</NavigationMenuLink>
<!-- Primitive: swap div for section -->
<Primitive as="section" class="container">
<slot />
</Primitive>
<!-- Create reusable base component -->
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
const props = withDefaults(defineProps<PrimitiveProps>(), {
as: 'div'
})
</script>
<template>
<Primitive v-bind="props">
<slot />
</Primitive>
</template>
<!-- Dialogs, Tooltips, Popovers need Portal -->
<DialogRoot v-model:open="open">
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<!-- Rendered in <body> — avoids z-index and overflow issues -->
<DialogOverlay class="fixed inset-0 bg-black/50" />
<DialogContent class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<DialogTitle>Title</DialogTitle>
<DialogClose>Close</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
<!-- Keep content in DOM even when closed (for CSS transitions) -->
<DialogContent force-mount v-show="open" class="data-[state=open]:animate-in">
<!-- Content always mounted, visibility controlled by data-state -->
</DialogContent>
CheckboxRoot, RadioGroupRoot + RadioGroupItem, SwitchRoot,
SliderRoot, NumberFieldRoot, PinInputRoot, TagsInputRoot
DialogRoot, AlertDialogRoot, PopoverRoot, TooltipRoot,
HoverCardRoot, SheetRoot (via DrawerRoot)
NavigationMenuRoot, MenubarRoot, ContextMenuRoot,
DropdownMenuRoot, TabsRoot
SelectRoot, ComboboxRoot, ListboxRoot
AccordionRoot, CollapsibleRoot
ScrollAreaRoot, SeparatorRoot, AvatarRoot, AspectRatioRoot
CalendarRoot, RangeCalendarRoot (uses @internationalized/date)
<script setup lang="ts">
import { ComboboxRoot, ComboboxVirtualizer, ComboboxItem } from 'reka-ui'
const items = ref(Array.from({ length: 10000 }, (_, i) => `Item ${i}`))
</script>
<template>
<ComboboxRoot>
<ComboboxContent>
<!-- Automatic virtualization for performance -->
<ComboboxVirtualizer
:options="items"
:estimate-size="32"
v-slot="{ option }"
>
<ComboboxItem :value="option">{{ option }}</ComboboxItem>
</ComboboxVirtualizer>
</ComboboxContent>
</ComboboxRoot>
</template>
Supported in: ComboboxRoot, ListboxRoot, TreeRoot
<script setup lang="ts">
import { useLocale, useDirection } from 'reka-ui'
// Global locale for date components
const { locale } = useLocale()
locale.value = 'pt-BR'
// RTL support
const { dir } = useDirection()
dir.value = 'ltr' // or 'rtl'
</script>
<script setup lang="ts">
import { CalendarRoot, CalendarGrid, CalendarCell } from 'reka-ui'
import { CalendarDate, today, getLocalTimeZone } from '@internationalized/date'
import { ref } from 'vue'
const value = ref(today(getLocalTimeZone()))
</script>
<template>
<CalendarRoot v-model="value" locale="pt-BR">
<CalendarGrid>
<CalendarCell v-for="date in dates" :date="date" />
</CalendarGrid>
</CalendarRoot>
</template>
# Via shadcn-vue CLI (reinstalls all components with Reka)
npx shadcn-vue@latest add --all
# Manual: swap imports
# Before: import { Dialog } from 'radix-vue'
# After: import { DialogRoot } from 'reka-ui'
# or: import { Dialog } from '@/components/ui/dialog' ← preferred
Main name changes:
| Radix Vue | Reka UI |
|---|---|
RadixDialog | DialogRoot |
import from 'radix-vue' | import from 'reka-ui' |
| Same API pattern | Same API pattern |
asChild + slot: do not add elements between the Reka component and the direct childv-model:open is reactive by reference — do not destroy and recreate the componentforceMount + v-show works; forceMount + v-if does not (defeats the purpose)<body> — scoped CSS may not reach the contentComboboxVirtualizer: estimateSize can be a function for variable heights (v2.7+)@internationalized/date is a peer dependency — install separately: npm i @internationalized/datenpx claudepluginhub pwdev-solucoes/pwdev-claude-marketplace --plugin pwdev-uiuxGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.