Headless UI, Radix Vue and Component Libraries Compared
AI generated
<v/>
{ }
Vue 3 · Component Libraries · Headless UI · Radix Vue
Headless UI, Radix Vue and
Component Libraries Compared

Choosing the right Vue 3 component library decides accessibility, bundle size and how much design control the team keeps. Headless UI, Radix Vue, shadcn/vue, PrimeVue and Naive UI follow fundamentally different philosophies, and this article shows which one is right for which context.

15 min read Headless UI · Radix Vue · shadcn/vue · PrimeVue · Naive UI Vue 3 · Tailwind CSS · ARIA

1. Headless vs. styled: the two core philosophies

The most fundamental difference between Vue 3 component libraries is not the number of components they ship, but the question of who controls the visual design. Headless component libraries like Headless UI and Radix Vue provide only logic and accessibility, with no CSS, no classes, no styles at all. Design is entirely up to the developer. Styled component libraries like PrimeVue, Naive UI or Vuetify ship finished, visual components with their own design system that can be adapted but not fully replaced.

This philosophy has direct consequences for customizability: headless libraries enable maximum design freedom and slot seamlessly into any design system or Tailwind setup. Styled libraries speed up development considerably when the built-in design is good enough or only needs minor adjustments. Between these two poles sits shadcn/vue: it ships pre-styled components as Tailwind CSS source code that you copy directly into your project and therefore own outright. That is a third philosophy, not installed but owned, and it is gaining growing popularity in the Vue community.

2. Headless UI for Vue: accessibility without style

Headless UI from Tailwind Labs is the best-known headless component library with Vue 3 support. It provides components for the most common interaction patterns: Dialog, Disclosure, Listbox, Menu, Popover, RadioGroup, Switch, Tab and Transition. Every component implements the corresponding ARIA pattern fully and correctly. Keyboard navigation, focus management and screen reader announcements are built in, without the developer having to set ARIA attributes manually.

Using Headless UI in Vue 3 requires that you add all visual styles yourself, typically with Tailwind CSS. That sounds like extra work, but it is the core of the concept: the logic (what the button does) is completely separated from the presentation (what it looks like). For teams that have their own design system or use Tailwind, Headless UI is ideal. For teams that want to get to a working UI quickly without styling every component from scratch, the learning curve is initially steeper than with styled libraries.


<!-- AccessibleDialog.vue, Headless UI Dialog with custom Tailwind styles -->
<template>
  <TransitionRoot appear :show="isOpen" as="template">
    <Dialog as="div" class="relative z-50" @close="closeModal">
      <TransitionChild
        as="template"
        enter="duration-200 ease-out" enter-from="opacity-0" enter-to="opacity-100"
        leave="duration-150 ease-in" leave-from="opacity-100" leave-to="opacity-0"
      >
        <!-- Overlay, styled with Tailwind, accessible via Headless UI Dialog -->
        <div class="fixed inset-0 bg-black/40 backdrop-blur-sm" />
      </TransitionChild>

      <div class="fixed inset-0 flex items-center justify-center p-4">
        <TransitionChild
          as="template"
          enter="duration-200 ease-out" enter-from="opacity-0 scale-95" enter-to="opacity-100 scale-100"
          leave="duration-150 ease-in" leave-from="opacity-100 scale-100" leave-to="opacity-0 scale-95"
        >
          <!-- Dialog panel, full focus trap and ARIA management by Headless UI -->
          <DialogPanel class="bg-white rounded-2xl shadow-xl p-6 max-w-md w-full">
            <DialogTitle class="text-lg font-bold text-slate-900">{{ title }}</DialogTitle>
            <DialogDescription class="text-sm text-slate-600 mt-2">{{ description }}</DialogDescription>
            <div class="mt-6 flex gap-3 justify-end">
              <button class="px-4 py-2 rounded-lg bg-slate-100 hover:bg-slate-200 text-sm font-medium" @click="closeModal">Cancel</button>
              <button class="px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 text-sm font-medium" @click="confirm">Confirm</button>
            </div>
          </DialogPanel>
        </TransitionChild>
      </div>
    </Dialog>
  </TransitionRoot>
</template>

<script setup>
import { Dialog, DialogPanel, DialogTitle, DialogDescription, TransitionRoot, TransitionChild } from '@headlessui/vue'

defineProps({ isOpen: Boolean, title: String, description: String })
const emit = defineEmits(['close', 'confirm'])

const closeModal = () => emit('close')
const confirm = () => emit('confirm')
<\/script>

3. Radix Vue: primitive building blocks for composables

Radix Vue is the Vue port of the popular Radix UI library from the React ecosystem. Unlike Headless UI, which offers full component abstractions, Radix Vue thinks in primitives: granular, composable building blocks that you combine following the composite pattern. A Dialog in Radix Vue consists of DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle and DialogClose, and each part is individually replaceable and customizable.

This granularity makes Radix Vue especially flexible for advanced use cases: you can use only the parts of a component you need and plug in your own implementations for the rest. Like Headless UI, Radix Vue ships no styles whatsoever; it is a pure accessibility and logic layer. The difference from Headless UI lies in the granularity of the abstraction and the breadth of supported components: Radix Vue covers significantly more interaction patterns, including more complex ones like Toolbar, NavigationMenu, Accordion, Collapsible, ContextMenu and Toast.

4. shadcn/vue: the copy-paste approach

shadcn/vue is not an npm package in the classic sense. Instead, you copy components via CLI (npx shadcn-vue@latest add button) directly into your own project, as source code that you own outright and can modify however you like. Internally, the components build on Radix Vue for accessibility and logic and use Tailwind CSS for styling. The result: fully styled, accessible components in your own codebase, without an npm update ever being able to change the design.

The shadcn/vue approach is especially attractive for teams that want a consistent design system but do not have the resources to build every component from scratch. You start with the default design and adjust the Tailwind classes to your own needs. Because the code lives in your own repository, it is no problem to fundamentally redesign a component, since there is no library API you could break. The downside: bug fixes and improvements from the shadcn/vue project have to be pulled in manually, since there is no npm update to run.

5. PrimeVue: a full component library

PrimeVue is the most extensive styled component library for Vue 3. With over 90 components, from simple buttons to complex DataTables with virtual scrolling, TreeSelect, FileUpload and Gantt charts, PrimeVue covers requirements that other libraries do not even attempt to address. The theming system lets you customize the entire design via CSS custom properties and preset configurations. PrimeVue 4 introduced Tailwind CSS compatibility through the "Tailwind" presets.

Using PrimeVue makes sense when an application needs many complex data components (DataTables, charts, calendars, drag-and-drop) and the team's design system is flexible enough to adapt to PrimeVue. For an application with a heavily individualized design system, PrimeVue can turn into a fight against the library: every customization pushes against its own default classes. In such cases, a headless approach is more maintainable in the long run despite the higher initial effort.

6. Naive UI: TypeScript-first with its own theming engine

Naive UI is a full Vue 3 component library focused on TypeScript support and a powerful, programmatic theming engine. Instead of CSS custom properties or Tailwind, Naive UI uses a JavaScript-object-based theme system that is evaluated at runtime. This enables dynamic theme switching without CSS variables and makes Naive UI particularly attractive for applications that need dark mode or user-configurable themes.

Naive UI offers excellent TypeScript types for all props and events, which makes it pleasant to use for TypeScript-heavy teams. The component palette is extensive and covers most enterprise requirements. The downside lies in theming complexity: the JavaScript-based theme system is less intuitive for simple adjustments than CSS-custom-properties approaches. For Tailwind-heavy projects, Naive UI is less well suited, since the library brings its own styling paradigm.

7. Accessibility: the decisive difference

Accessibility is the area where headless component libraries like Headless UI and Radix Vue differ most strongly from simple HTML wrappers. Correct ARIA patterns for complex widgets like comboboxes, date pickers or trees are difficult and time-consuming to implement yourself. The WAI-ARIA Authoring Practices Guide describes dozens of keyboard navigation patterns that all have to be correctly implemented for a widget to be usable by screen reader users.

Headless UI and Radix Vue take exactly this implementation burden off your hands. They implement the ARIA patterns from the WAI-ARIA standard correctly and consistently, without the developer having to set ARIA attributes manually or write keyboard handlers. Styled libraries like PrimeVue and Naive UI also offer good accessibility support, but implementation quality varies by component. Anyone who treats accessibility as a non-negotiable requirement, for example for public sector applications or e-commerce platforms with legal accessibility obligations, should carefully check the accessibility quality of every component library.


<!-- AccessibleSelect.vue, Radix Vue Select primitive with custom styling -->
<template>
  <SelectRoot v-model="selected">
    <SelectTrigger
      class="flex items-center justify-between w-full px-4 py-2 bg-white border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
      aria-label="Select country"
    >
      <SelectValue placeholder="Choose a country..." />
      <SelectIcon><ChevronDownIcon class="w-4 h-4 text-slate-500" /></SelectIcon>
    </SelectTrigger>

    <!-- Portal renders outside DOM tree to avoid overflow clipping -->
    <SelectPortal>
      <SelectContent
        class="bg-white border border-slate-200 rounded-xl shadow-lg p-1 z-50"
        position="popper"
        :side-offset="4"
      >
        <SelectViewport>
          <SelectItem
            v-for="option in options"
            :key="option.value"
            :value="option.value"
            class="flex items-center px-4 py-2 text-sm rounded-lg cursor-pointer hover:bg-green-50 focus:bg-green-50 data-[highlighted]:bg-green-50 outline-none"
          >
            <SelectItemText>{{ option.label }}</SelectItemText>
            <!-- Checkmark shown automatically for selected item via Radix Vue -->
            <SelectItemIndicator class="ml-auto">
              <CheckIcon class="w-4 h-4 text-green-600" />
            </SelectItemIndicator>
          </SelectItem>
        </SelectViewport>
      </SelectContent>
    </SelectPortal>
  </SelectRoot>
</template>

<script setup>
// Radix Vue handles: keyboard navigation, ARIA attributes, focus management, portal rendering
import { SelectRoot, SelectTrigger, SelectValue, SelectIcon, SelectPortal, SelectContent, SelectViewport, SelectItem, SelectItemText, SelectItemIndicator } from 'radix-vue'
const selected = defineModel()
defineProps({ options: Array })
<\/script>

8. Integrating into your own design system

Integrating a Vue 3 component library into an existing design system is the practically most important dimension when choosing a library. Headless libraries integrate smoothly by definition: you bring your own Tailwind classes. shadcn/vue starts with Tailwind defaults and can be customized arbitrarily through direct code modification. PrimeVue and Naive UI have their own theming systems that can work well with a design system but require specific integration effort.

An often overlooked aspect of design system integration: tokens. Modern design systems define colors, spacing and typography as design tokens, either as CSS custom properties or as Tailwind configuration values. Headless UI and Radix Vue use these tokens automatically because all styles come from the developer. shadcn/vue uses CSS custom properties as its token system and therefore couples well with a token-based design system. PrimeVue's own token layer has to be kept in sync with your own design token system, an effort that should not be underestimated on larger projects.

9. Direct comparison: all libraries at a glance

All the component libraries for Vue 3 discussed here have their place. The decision depends on project requirements, team know-how and design system requirements. Here is a direct comparison of the most important dimensions:

Library Type Accessibility Tailwind compatibility Recommended for
Headless UI Headless Excellent Native Tailwind teams with their own design
Radix Vue Headless primitives Excellent Native Granular control, shadcn base
shadcn/vue Copy-paste styled Very good (via Radix) Native Fast start with a customizable design
PrimeVue Styled (full) Good Possible via preset Enterprise with complex data components
Naive UI Styled (TS-first) Good Own system TypeScript teams with dynamic theming

The decision is rarely binary. Many projects use shadcn/vue as the base for most components and pull in a specific library for special requirements, for example a DateRangePicker or a feature-rich DataTable. What matters is that the core architecture is built on a library that fits the team's design system, in order to avoid theming conflicts.

Mironsoft

Vue 3 development with Headless UI, Radix Vue and design system integration

The right component library for your Vue project?

We analyze your design system requirements and accessibility goals and recommend the right Vue 3 component strategy, from Headless UI and Radix Vue to shadcn/vue and PrimeVue.

Library audit

Analysis of existing UI stacks for accessibility gaps and design system conflicts

Headless setup

Integrating Radix Vue or Headless UI into your Tailwind design system, with ready-made base components

shadcn/vue migration

Migrating an existing library to shadcn/vue, including theme adaptation and an accessibility review

10. Summary

The choice between Headless UI, Radix Vue, shadcn/vue, PrimeVue and Naive UI is not a question of better or worse, but of requirements and context. Headless libraries give maximum design freedom and excellent accessibility, but require the team to bring all styles itself. shadcn/vue balances this trade-off elegantly: finished, accessible components as modifiable source code in your own repository. PrimeVue and Naive UI significantly speed up development of complex data components, but require more effort for deep design customization.

For teams with Tailwind CSS and their own design system, Headless UI or Radix Vue (directly or via shadcn/vue) is the most future-proof choice: no external styles, full control, excellent accessibility. For enterprise applications with complex data components and less strict design requirements, PrimeVue or Naive UI is the most productive option. In both cases, the accessibility quality of the library is not optional, it is a baseline requirement.

Vue 3 component libraries, the key points at a glance

Headless approach

Headless UI and Radix Vue: only logic and ARIA, no CSS. Maximum design freedom, native Tailwind compatibility, excellent accessibility.

shadcn/vue

Copy-paste approach: Radix Vue plus Tailwind code directly in your project. Fully modifiable, no npm-update risk, accessibility by default.

Styled libraries

PrimeVue for many data components, Naive UI for TypeScript-first teams. Their own theming systems require more effort to customize for individual designs.

Decision criteria

Design freedom, accessibility requirements, Tailwind usage, component complexity and team know-how decide the right choice.

11. FAQ: Headless UI, Radix Vue and Component Libraries

1Headless UI vs. Radix Vue?
Headless UI: full component abstractions. Radix Vue: granular primitives to assemble yourself. Both headless, Radix Vue gives more granular control.
2What does headless mean?
Only logic and ARIA accessibility, no CSS. Design is entirely up to the developer, ideal for custom design systems and Tailwind CSS.
3shadcn/vue, a real library?
Not a classic npm package. Components are copied via CLI as source code, fully modifiable, no update risk.
4When to choose PrimeVue?
Enterprise apps with complex data components: DataTables, charts, TreeSelect, FileUpload. Saves significant time when the design is flexible.
5Headless UI accessibility quality?
WAI-ARIA patterns fully implemented: keyboard navigation, focus management, screen reader announcements, all without manual ARIA.
6Headless UI with Tailwind?
Yes, developed by Tailwind Labs. No CSS of its own, all styles with Tailwind classes, no conflicts.
7shadcn/vue vs. direct Radix Vue?
shadcn/vue equals Radix Vue plus ready-styled Tailwind components. Faster start, but code lives in your own project for full freedom to modify.
8Radix Vue in a Tailwind design system?
Add your own Tailwind classes. For highlighted/selected states, use Radix data attributes in Tailwind selectors, for example data-[highlighted]:bg-green-50.
9Naive UI with Tailwind?
Its own JavaScript theming system, no Tailwind support by default. Both together are possible, but styling stays separate, less suited for Tailwind projects.
10Recommendation for a new Vue 3 plus Tailwind project?
shadcn/vue as a copy-paste base (Radix Vue plus Tailwind). For specific data components, add PrimeVue as a supplement.