Index of every page on this site: llms.txt
All skills
nuxt avatar

/nuxt-ui

@bcc09c0 official
by nuxtnuxt/ui7k stars
1,152
Run it yourself

Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites, and chat interfaces.

Use this Skill: https://skilld.dev/gh/nuxt/ui/nuxt-ui

Nothing lands on disk. Nothing to clean up.

Fork this Skill

Edit a local copy. It keeps the author and licence.

referencesrecipesdata-tables.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Data Tables

Complete patterns for displaying and managing tabular data.

Basic table

<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'

const data = ref([
  { name: 'Alice', email: 'alice@example.com', role: 'Admin' },
  { name: 'Bob', email: 'bob@example.com', role: 'Editor' }
])

const columns: TableColumn<typeof data.value[number]>[] = [{
  accessorKey: 'name',
  header: 'Name'
}, {
  accessorKey: 'email',
  header: 'Email'
}, {
  accessorKey: 'role',
  header: 'Role'
}]
</script>

<template>
  <UTable :data="data" :columns="columns" />
</template>

With search and filters (dashboard)

<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'

const search = ref('')
const roleFilter = ref('All')

const rows = ref([
  { name: 'Alice', email: 'alice@example.com', role: 'Admin', status: 'Active' },
  { name: 'Bob', email: 'bob@example.com', role: 'Editor', status: 'Inactive' }
])

const columns: TableColumn[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
  { accessorKey: 'role', header: 'Role' },
  { accessorKey: 'status', header: 'Status' },
  { id: 'actions' }
]

const filteredRows = computed(() => {
  return rows.value.filter(row => {
    const matchesSearch = !search.value || row.name.toLowerCase().includes(search.value.toLowerCase())
    const matchesRole = roleFilter.value === 'All' || row.role === roleFilter.value
    return matchesSearch && matchesRole
  })
})
</script>

<template>
  <UDashboardPanel>
    <template #header>
      <UDashboardNavbar title="Users" />

      <UDashboardToolbar>
        <template #left>
          <UInput v-model="search" icon="i-lucide-search" placeholder="Search users..." />
        </template>
        <template #right>
          <USelect v-model="roleFilter" :items="['All', 'Admin', 'Editor', 'Viewer']" />
        </template>
      </UDashboardToolbar>
    </template>

    <template #body>
      <UTable :data="filteredRows" :columns="columns">
        <template #status-cell="{ row }">
          <UBadge :color="row.original.status === 'Active' ? 'success' : 'neutral'" :label="row.original.status" variant="subtle" />
        </template>

        <template #actions-cell="{ row }">
          <UDropdownMenu
            :items="[
              [{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit(row.original) }],
              [{ label: 'Delete', icon: 'i-lucide-trash', color: 'error', onSelect: () => remove(row.original) }]
            ]"
          >
            <UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" />
          </UDropdownMenu>
        </template>
      </UTable>
    </template>
  </UDashboardPanel>
</template>

With row selection

Row selection uses TanStack Table's rowSelection state, a Record<string, boolean> keyed by row ID. The default ID is the row index. Provide getRowId with a stable unique string ID when replacing data, filtering it before passing it to the table, or fetching server pages.

<script setup lang="ts">
const table = useTemplateRef('table')
const rowSelection = ref<Record<string, boolean>>({})
</script>

<template>
  <UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" :get-row-id="row => row.id" />

  <div class="px-4 py-3.5 text-sm text-muted">
    {{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
    {{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
  </div>
</template>

This example assumes each record has a unique string id. With server-side pagination, selected row models only include records in the current input data. For cross-page bulk actions, keep the selected IDs in rowSelection rather than deriving the full selection from getSelectedRowModel() or getFilteredSelectedRowModel(). Decide whether page or filter changes should clear selection.

Add a checkbox column using the h function. Use tri-state modelValue (true, false, or 'indeterminate') for the "select all" header:

import { h } from 'vue'

const UCheckbox = resolveComponent('UCheckbox')

const columns: TableColumn[] = [{
  id: 'select',
  header: ({ table }) => h(UCheckbox, {
    'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
    'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
    'aria-label': 'Select all'
  }),
  cell: ({ row }) => h(UCheckbox, {
    'modelValue': row.getIsSelected(),
    'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
    'aria-label': 'Select row'
  })
},
// ... other columns
]

With pagination

Use v-model:pagination on UTable with TanStack's getPaginationRowModel, then wire UPagination to the table API. UPagination's total is total items (not pages) — it calculates page count from total / items-per-page.

<script setup lang="ts">
import { getPaginationRowModel } from '@tanstack/vue-table'

const table = useTemplateRef('table')

const pagination = ref({
  pageIndex: 0,
  pageSize: 5
})
</script>

<template>
  <UTable
    ref="table"
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"
  />

  <div class="flex justify-end p-4">
    <UPagination
      :page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
      :items-per-page="table?.tableApi?.getState().pagination.pageSize"
      :total="table?.tableApi?.getFilteredRowModel().rows.length"
      @update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"
    />
  </div>
</template>

With async data (Nuxt)

Use status === 'pending' || status === 'idle' for loading state — idle covers the initial render before useLazyFetch starts.

<script setup lang="ts">
const { data, status } = useLazyFetch('/api/users', { server: false })
</script>

<template>
  <UTable :data="data" :columns="columns" :loading="status === 'pending' || status === 'idle'" />
</template>

For server-side pagination:

<script setup lang="ts">
const page = ref(1)

const { data, status } = await useAsyncData(
  'users',
  () => $fetch('/api/users', { query: { page: page.value } }),
  { watch: [page] }
)
</script>

<template>
  <UTable :data="data?.items" :columns="columns" :loading="status === 'pending'" />

  <div class="flex justify-end p-4">
    <UPagination v-model:page="page" :total="data?.total" :items-per-page="data?.pageSize" />
  </div>
</template>

Updating large datasets

UTable deep-watches data by default. For rows managed through array replacement, disable deep watching with :watch-options="{ deep: false }". Replace the array and each changed row so the table refreshes its row models.

<script setup lang="ts">
const rows = shallowRef([{ id: '1', name: 'Alice' }])

function renameRow(id: string, name: string) {
  rows.value = rows.value.map(row => row.id === id ? { ...row, name } : row)
}
</script>

<template>
  <UTable :data="rows" :watch-options="{ deep: false }" />
</template>

With deep watching disabled, do not rely on push(), splice(), or nested edits alone to refresh table row models. Keep the default watcher when in-place mutation is part of the update model.

The separate virtualize option limits rendered rows for large datasets. It does not disable deep watching. Check the installed Nuxt UI version for virtualization support and its sticky or pinning restrictions.

Tips

  • Table is built on TanStack Table v8 — columns use ColumnDef format with accessorKey, header, cell
  • Use #<column>-cell and #<column>-header template slots to customize rendering with Vue templates
  • Alternatively, use the h function inside header and cell column properties for inline rendering
  • Row data in slots is accessed via row.original (not row directly)
  • Use v-model:row-selection for selection, v-model:sorting for sort state
  • Wrap tables in UDashboardPanel with #header toolbar for the dashboard pattern
  • For empty states, use the #empty slot

Source: SKILL.md on GitHub

skilld matched fixed text patterns in SKILL.md and file names. Patterns miss obfuscated code.

skilld run checks every file with the same patterns. It asks for approval before it loads a Skill with a behavior marked Needs approval.

No alerts10d5 checks · Risk SAFE
  • Gen Agent Trust Hub10d

    The skill provides comprehensive documentation and development patterns for the Nuxt UI v4 component library. It includes configurations for official MCP servers and code recipes for common UI tasks using standard, well-known web technologies.

  • Socket10d

    No alerts

  • Snyk10d

    Risk: LOW · No issues

  • Runlayer8mo

    9 files scanned · No issues

  • ZeroLeaks6mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at bcc09c0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 5 days ago.

Activeupdated 3 months ago
  • Nuxt
  • Vue
  • tailwindcss
  • components
  • ui
  • forms
  • theming
  • accessibility

README badge

README badge for nuxt/ui

Provides 125+ accessible Vue components built on Tailwind CSS and Reka UI, with semantic color tokens and theming support. Use for building interfaces in Nuxt, Vue, Laravel Inertia, or AdonisJS — forms, dashboards, documentation sites, and chat UIs.

Generated from the current SKILL.md.

Does this work with Vue outside of Nuxt?
Yes. @nuxt/ui v4 works with Vue (Vite), Laravel (Vite + Inertia), and AdonisJS (Vite + Inertia). Installation differs slightly but uses the same component library.
How do I find out what props and slots a component accepts?
Use the Nuxt UI MCP server with the `get_component` tool for full documentation, or `get_component_metadata` for a lightweight props/slots/events reference. The skill also points you to generated theme files in `.nuxt/ui/<component>.ts`.
Can I customize the theme to match my brand?
Yes. The skill includes a design-system reference that covers semantic colors, theming, and brand customization. Components use semantic color names like `text-default` and `bg-elevated` rather than raw Tailwind colors.
What if I need help building a specific layout like a dashboard or docs site?
The skill includes full layout references for dashboards, docs sites, landing pages, chat interfaces, and editors. Load the relevant reference file before coding to follow the established patterns.
Does this include form validation?
Yes. The forms reference covers validation, field layout, error handling, and Standard Schema integration for both client and server-side validation.

Generated from the current SKILL.md. These answers refresh after source changes.