---
title: "Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none · vuejs.school"
description: "One page with a data table, a dialog and a form, built in Nuxt UI, PrimeVue and Vuetify, then compared, plus when to build your own with Reka UI."
canonical_url: "https://vuejs.school/community/blog/component-libraries-in-2026"
meta:
  "og:description": "One page with a data table, a dialog and a form, built in Nuxt UI, PrimeVue and Vuetify, then compared, plus when to build your own with Reka UI."
  "og:image": "https://vuejs.school/_og/d/a_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZQ%2Cdescription_~T25lIHBhZ2Ugd2l0aCBhIGRhdGEgdGFibGUsIGEgZGlhbG9nIGFuZCBhIGZvcm0sIGJ1aWx0IGluIE51eHQgVUksIFByaW1lVnVlIGFuZCBWdWV0aWZ5LCB0aGVuIGNvbXBhcmVkLCBwbHVzIHdoZW4gdG8gYnVpbGQgeW91ciBvd24gd2l0aCBSZWthIFVJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jb21wb25lbnQtbGlicmFyaWVzLWluLTIwMjYi%2Cs_oL33ZIgC7WF8RgH_.png"
  "og:image:alt": "Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZQ%2Cdescription_~T25lIHBhZ2Ugd2l0aCBhIGRhdGEgdGFibGUsIGEgZGlhbG9nIGFuZCBhIGZvcm0sIGJ1aWx0IGluIE51eHQgVUksIFByaW1lVnVlIGFuZCBWdWV0aWZ5LCB0aGVuIGNvbXBhcmVkLCBwbHVzIHdoZW4gdG8gYnVpbGQgeW91ciBvd24gd2l0aCBSZWthIFVJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jb21wb25lbnQtbGlicmFyaWVzLWluLTIwMjYi%2Cs_oL33ZIgC7WF8RgH_.png"
  "twitter:image:alt": "Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Build the page with Nuxt UI 4

Nuxt UI is built on Reka UI, a set of unstyled, accessible Vue primitives, and on Tailwind CSS. Its [introduction](https://ui.nuxt.com/docs/getting-started) lists 125 or more components and says it works in plain Vue with Vite as well as in Nuxt. Since version 4 it is a single free, open-source package; the former Pro components are included.

To add it to a Nuxt app:

1. Run `npm install @nuxt/ui tailwindcss zod` (Zod is for the form's schema).
2. Add `modules: ['@nuxt/ui']` to `nuxt.config.ts`.
3. Create `app/assets/css/main.css` with `@import "tailwindcss";` and `@import "@nuxt/ui";`, and load it with `css: ['~/assets/css/main.css']` in `nuxt.config.ts`.
4. Wrap your app in `<UApp>` in `app/app.vue`.

Then the page:

*app/pages/customers.vue*

```
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent, TableColumn } from '@nuxt/ui'

type Customer = { id: number, name: string, email: string }

const customers = ref<Customer[]>([
  { id: 1, name: 'Asha Patel', email: 'asha@example.com' },
  { id: 2, name: 'Tom Cook', email: 'tom@example.com' },
])
const columns: TableColumn<Customer>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
]

const schema = z.object({
  name: z.string().min(1, 'Enter a name'),
  email: z.email('Enter a valid email'),
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ name: undefined, email: undefined })
const open = ref(false)

function onSubmit(event: FormSubmitEvent<Schema>) {
  customers.value.push({ id: Date.now(), ...event.data })
  open.value = false
}
</script>

<template>
  <UModal v-model:open="open" title="New customer">
    <UButton label="Add customer" />
    <template #body>
      <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
        <UFormField label="Name" name="name">
          <UInput v-model="state.name" />
        </UFormField>
        <UFormField label="Email" name="email">
          <UInput v-model="state.email" type="email" />
        </UFormField>
        <UButton type="submit" label="Save" />
      </UForm>
    </template>
  </UModal>
  <UTable :data="customers" :columns="columns" />
</template>
```

The button in `UModal`'s default slot becomes the dialog's trigger, so it needs no click handler, and the form goes in the `#body` slot. `UTable` is built on TanStack Table, so sorting, filtering, pagination and row selection come from the same column definitions.

`UForm` checks `state` against the schema, shows each error under the `UFormField` with the same `name`, and calls `onSubmit` only when the data is valid. It accepts any Standard Schema library (a shared interface that Zod, Valibot and others implement); `z.email()` is Zod 4's email check. Nothing is styled by hand: the look comes from Tailwind classes inside the components, which you override through props, the `ui` prop or your app config.

## Build the page with PrimeVue 4

PrimeVue is made by PrimeTek, the company behind PrimeFaces, PrimeNG and PrimeReact, and it has no paywall; paid add-ons such as templates and a Figma kit are optional. It has two modes:

- **Styled mode** ships themes built from design tokens (named values for colours, spacing and the like), with presets called Aura, Material, Lara and Nora.
- **Unstyled mode** removes all CSS and leaves you the behaviour and accessibility, to style with Tailwind or anything else.

Its pass-through API lets you add classes and attributes to any element inside a component.

To add it to a Nuxt app:

1. Run `npm install primevue @primeuix/themes` and `npm install -D @primevue/nuxt-module`.
2. In `nuxt.config.ts`, add `'@primevue/nuxt-module'` to `modules` and set `primevue: { options: { theme: { preset: Aura } } }`, where `Aura` is imported from `@primeuix/themes/aura`.

The module auto-imports PrimeVue's components, so the page needs no imports:

*app/pages/customers.vue*

```
<script setup lang="ts">
type Customer = { id: number, name: string, email: string }

const customers = ref<Customer[]>([
  { id: 1, name: 'Asha Patel', email: 'asha@example.com' },
  { id: 2, name: 'Tom Cook', email: 'tom@example.com' },
])
const open = ref(false)
const form = reactive({ name: '', email: '' })

function save() {
  if (!form.name || !form.email.includes('@')) return
  customers.value.push({ id: Date.now(), ...form })
  Object.assign(form, { name: '', email: '' })
  open.value = false
}
</script>

<template>
  <Button label="Add customer" @click="open = true" />
  <Dialog v-model:visible="open" modal header="New customer">
    <form @submit.prevent="save">
      <label for="customer-name">Name</label>
      <InputText id="customer-name" v-model="form.name" />
      <label for="customer-email">Email</label>
      <InputText id="customer-email" v-model="form.email" type="email" />
      <Button type="submit" label="Save" />
    </form>
  </Dialog>
  <DataTable :value="customers">
    <Column field="name" header="Name" />
    <Column field="email" header="Email" />
  </DataTable>
</template>
```

Here the button opens the dialog with its own `@click`; the close button and Escape close it through `v-model:visible`. `DataTable` with `Column` children is PrimeVue's signature component, with sorting, filtering, row grouping, export and virtual scrolling built in.

The form is plain `v-model`, and `save()` silently refuses an empty name or an email without `@`. PrimeVue also has a separate `@primevue/forms` package for when you need validation state across fields, such as an error message under each one.

## Build the page with Vuetify 4

Vuetify implements Google's Material Design. Version 4.0.0 was published on 23 February, and its [release notes](https://github.com/vuetifyjs/vuetify/releases/tag/v4.0.0) include these changes:

- Typography and elevation (the shadow levels) follow Material Design 3.
- The grid system is overhauled.
- All styles sit in CSS cascade layers ( `@layer`) instead of relying on `!important`.
- The default theme is `system`, which follows the user's light or dark setting.
- Buttons no longer force uppercase text.

In Nuxt, run `npx nuxi@latest module add vuetify-nuxt-module`; versions 0.19.3 and later of the module accept Vuetify 4. Then wrap your app in `<v-app>` in `app/app.vue` and write the page:

*app/pages/customers.vue*

```
<script setup lang="ts">
import type { DataTableHeader, SubmitEventPromise } from 'vuetify'

type Customer = { id: number, name: string, email: string }

const customers = ref<Customer[]>([
  { id: 1, name: 'Asha Patel', email: 'asha@example.com' },
  { id: 2, name: 'Tom Cook', email: 'tom@example.com' },
])
const headers: DataTableHeader[] = [
  { title: 'Name', key: 'name' },
  { title: 'Email', key: 'email' },
]
const open = ref(false)
const name = ref('')
const email = ref('')
const required = (value: string) => !!value || 'Required'

async function save(event: SubmitEventPromise) {
  const { valid } = await event
  if (!valid) return
  customers.value.push({ id: Date.now(), name: name.value, email: email.value })
  open.value = false
}
</script>

<template>
  <v-dialog v-model="open" max-width="420">
    <template #activator="{ props: activatorProps }">
      <v-btn v-bind="activatorProps" text="Add customer" />
    </template>
    <v-card title="New customer">
      <v-card-text>
        <v-form @submit.prevent="save">
          <v-text-field v-model="name" label="Name" :rules="[required]" />
          <v-text-field v-model="email" label="Email" type="email" :rules="[required]" />
          <v-btn type="submit" text="Save" />
        </v-form>
      </v-card-text>
    </v-card>
  </v-dialog>
  <v-data-table :headers="headers" :items="customers" />
</template>
```

Validation lives on the fields as `rules`, and the form's submit event doubles as a promise that resolves with `{ valid, errors }`. The activator slot passes the ARIA attributes that connect the button to its dialog, which is why you bind `activatorProps` rather than adding a click handler.

## Compare styling, accessibility, size, Nuxt integration and lock-in

**Styling.** Nuxt UI is Tailwind all the way down, so if your team already writes Tailwind it feels like your own code. PrimeVue lets you choose: a token-based theme you adjust, or unstyled components you dress yourself. Vuetify looks like Material Design unless you work hard against it; theme colours and Sass variables change the palette, not the personality.

**Accessibility.** All three take it seriously, in different words. Nuxt UI inherits Reka UI's implementation of the WAI-ARIA patterns: focus trapping, keyboard navigation, screen reader labels. PrimeVue states WCAG 2.1 AA compliance and documents keyboard and screen reader support on every component page. Vuetify's guide says its components provide keyboard interactions for all mouse actions and use semantic HTML where it applies. None of them can rescue an input without a label, so labels are still your job.

**Size.** All three tree-shake, so you ship only the components you use, and the Nuxt modules handle that through auto-imports. Their CSS differs: Nuxt UI emits Tailwind utilities, PrimeVue's styled mode builds its theme CSS from tokens, and Vuetify ships its own stylesheet. Published comparisons go stale quickly; build this page with each and compare the sizes `nuxt build` prints.

**Nuxt integration.** Nuxt UI is made by the Nuxt team and registers icons, fonts and colour mode for you. PrimeVue and Vuetify both have solid modules, maintained by their own teams and the community.

**Lock-in.** Every library leaks into every template. Leaving one means rewriting each page that uses it. Unstyled PrimeVue and Reka-based Nuxt UI keep your visual design in your own classes, which makes a later move a little cheaper.

## Build your own with Reka UI, or use no library

The fourth option is no library at all, or rather, only the hard part of one. Reka UI 2.9 provides behaviour and accessibility with no styles: the dialog traps focus, closes on Escape, wires up the title for screen readers and makes the page behind it inert (clicks and screen readers cannot reach it). You add the Tailwind classes, here in a reusable dialog component:

*app/components/AppDialog.vue*

```
<script setup lang="ts">
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, DialogTrigger } from 'reka-ui'

defineProps<{ title: string, description: string }>()
const open = defineModel<boolean>('open', { default: false })
</script>

<template>
  <DialogRoot v-model:open="open">
    <DialogTrigger as-child>
      <slot name="trigger" />
    </DialogTrigger>
    <DialogPortal>
      <DialogOverlay class="fixed inset-0 bg-black/40" />
      <DialogContent class="fixed top-1/2 left-1/2 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6">
        <DialogTitle class="text-lg font-semibold">{{ title }}</DialogTitle>
        <DialogDescription class="mt-1 text-sm text-gray-600">{{ description }}</DialogDescription>
        <div class="mt-4">
          <slot />
        </div>
        <DialogClose class="absolute top-4 right-4 rounded px-2 py-1 hover:bg-gray-100" aria-label="Close">×</DialogClose>
      </DialogContent>
    </DialogPortal>
  </DialogRoot>
</template>
```

A page passes its own button in the `#trigger` slot and its form in the default slot, and binds `v-model:open`. `as-child` makes the trigger use that button instead of wrapping it in another one. You would build a table the same way with plain `<table>` markup, and a form with native inputs. It costs more time per component, and in return every pixel and every dependency is yours.

For the smallest projects, even primitives can be more than you need. The browser's own `<dialog>` element, opened with `showModal()`, already makes the page behind it inert and closes on Escape, and a plain `<table>` with a few Tailwind classes is enough for a list of twenty customers. Reach for Reka UI when you need the harder widgets: comboboxes, menus, date pickers and anything with roving focus (arrow keys move focus within a group).

## Which one to pick

- **A Nuxt app, a team that writes Tailwind, a dashboard or SaaS product:** Nuxt UI. It is the shortest path from idea to a polished screen.
- **A data-heavy business app, especially without a designer:** PrimeVue in styled mode with Aura. Its table and form components go deepest, and the theme looks finished from day one.
- **A product that should look like Material Design, or an existing Vuetify 3 app:** Vuetify 4. Upgrading keeps your knowledge, and the new CSS layers make overrides much less painful.
- **A strong brand design system, or a component kit shared across apps:** build on Reka UI and Tailwind. The primitives handle accessibility; your design stays your own.
- **A marketing site or a small app with a few buttons and one form:** no library. Native elements with Tailwind are lighter than any of the above.

**Check it stuck**3 questions from the Component libraries quiz.

[**Practice now**](https://vuejs.school/learn/styling/component-libraries?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 7 min read**Accessible components: focus, keyboard and ARIA**](https://vuejs.school/community/blog/accessible-components-focus-keyboard-and-aria) [Tutorial · 7 min read**Dark mode and theming with CSS variables**](https://vuejs.school/community/blog/dark-mode-and-theming-with-css-variables)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/component-libraries-in-2026#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-05-14","description":"One page with a data table, a dialog and a form, built in Nuxt UI, PrimeVue and Vuetify, then compared, plus when to build your own with Reka UI.","name":"Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none","url":"https://vuejs.school/community/blog/component-libraries-in-2026","breadcrumb":{"@id":"https://vuejs.school/community/blog/component-libraries-in-2026#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/component-libraries-in-2026"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/component-libraries-in-2026#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-05-14","description":"One page with a data table, a dialog and a form, built in Nuxt UI, PrimeVue and Vuetify, then compared, plus when to build your own with Reka UI.","headline":"Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZQ%2Cdescription_~T25lIHBhZ2Ugd2l0aCBhIGRhdGEgdGFibGUsIGEgZGlhbG9nIGFuZCBhIGZvcm0sIGJ1aWx0IGluIE51eHQgVUksIFByaW1lVnVlIGFuZCBWdWV0aWZ5LCB0aGVuIGNvbXBhcmVkLCBwbHVzIHdoZW4gdG8gYnVpbGQgeW91ciBvd24gd2l0aCBSZWthIFVJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jb21wb25lbnQtbGlicmFyaWVzLWluLTIwMjYi%2Cs_oL33ZIgC7WF8RgH_.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/component-libraries-in-2026#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/component-libraries-in-2026#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/component-libraries-in-2026#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Component libraries in 2026: Nuxt UI, PrimeVue, Vuetify and when to use none","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZQ%2Cdescription_~T25lIHBhZ2Ugd2l0aCBhIGRhdGEgdGFibGUsIGEgZGlhbG9nIGFuZCBhIGZvcm0sIGJ1aWx0IGluIE51eHQgVUksIFByaW1lVnVlIGFuZCBWdWV0aWZ5LCB0aGVuIGNvbXBhcmVkLCBwbHVzIHdoZW4gdG8gYnVpbGQgeW91ciBvd24gd2l0aCBSZWthIFVJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jb21wb25lbnQtbGlicmFyaWVzLWluLTIwMjYi%2Cs_oL33ZIgC7WF8RgH_.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZSDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_~Q29tcG9uZW50IGxpYnJhcmllcyBpbiAyMDI2OiBOdXh0IFVJLCBQcmltZVZ1ZSwgVnVldGlmeSBhbmQgd2hlbiB0byB1c2Ugbm9uZQ%2Cdescription_~T25lIHBhZ2Ugd2l0aCBhIGRhdGEgdGFibGUsIGEgZGlhbG9nIGFuZCBhIGZvcm0sIGJ1aWx0IGluIE51eHQgVUksIFByaW1lVnVlIGFuZCBWdWV0aWZ5LCB0aGVuIGNvbXBhcmVkLCBwbHVzIHdoZW4gdG8gYnVpbGQgeW91ciBvd24gd2l0aCBSZWthIFVJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jb21wb25lbnQtbGlicmFyaWVzLWluLTIwMjYi%2Cs_oL33ZIgC7WF8RgH_.png"}]}
```