Docs
Getting started

Vue and Nuxt quick start

A complete native Vue table, shared Query setup, and Nuxt integration.

Build the same product table with native Vue configuration and a shared Vue Query client. Install the Vue registry before copying these files. For the shared mental model, read core concepts.

Project structure

products/
  shared/products.ts
  vue/product-config.ts
  vue/product-actions.ts
  vue/products.vue
  vue/main.ts

1. Define records

products.ts
export interface Product extends Record<string, unknown> {
  active: boolean;
  id: string;
  image: string;
  name: string;
  price: number;
  /** Civil dates, inclusive at both ends: the window a batch is being restocked. */
  restockFrom: string;
  restockTo: string;
  status: "draft" | "active" | "archived";
  stock: number;
  tags: string[];
  updatedAt: string;
}

export const products: Product[] = [
  {
    id: "desk",
    name: "Oak desk",
    price: 420,
    stock: 12,
    status: "active",
    active: true,
    tags: ["office", "wood"],
    image: "https://placehold.co/640x480/png?text=Oak+desk",
    restockFrom: "2026-09-14",
    restockTo: "2026-09-18",
    updatedAt: "2026-09-01T09:00:00Z",
  },
  {
    id: "lamp",
    name: "Desk lamp",
    price: 65,
    stock: 0,
    status: "draft",
    active: false,
    tags: ["office"],
    image: "https://placehold.co/640x480/png?text=Desk+lamp",
    restockFrom: "2026-09-17",
    restockTo: "2026-09-24",
    updatedAt: "2026-09-02T09:00:00Z",
  },
  {
    id: "chair",
    name: "Studio chair",
    price: 180,
    stock: 6,
    status: "active",
    active: true,
    tags: ["office", "seating"],
    image: "https://placehold.co/640x480/png?text=Studio+chair",
    restockFrom: "2026-09-23",
    restockTo: "2026-10-02",
    updatedAt: "2026-09-03T09:00:00Z",
  },
];

2. Configure the table

product-config.ts
import { defineTableConfig } from "@/components/ui/yayaw-table-vue";

export const productConfig = defineTableConfig({
  id: "products",
  translations: { namespace: "products", keys: {} },
  columns: {
    definitions: [
      { id: "name", header: "Name", type: "text" },
      { id: "price", header: "Price", type: "number" },
      { id: "stock", header: "Stock", type: "number" },
      {
        id: "status",
        header: "Status",
        type: "select",
        options: [
          { label: "Draft", value: "draft" },
          { label: "Active", value: "active" },
          { label: "Archived", value: "archived" },
        ],
      },
    ],
    order: ["name", "price", "stock", "status"],
    visible: ["name", "price", "stock", "status"],
    mandatory: ["name"],
    sort: [{ id: "name", desc: false }],
  },
  table: {
    allowCreate: false,
    allowEdit: false,
    allowDelete: false,
    allowDuplicate: false,
    allowBulkEdit: false,
    allowBulkDelete: false,
    enableRowSelection: false,
    enableViews: false,
    syncUrl: false,
    defaultPageSize: 10,
  },
});

export const getTableConfig = (tableType: string) =>
  tableType === "products" ? productConfig : undefined;

3. Connect list actions

This adapter implements the same request contract as React. Search covers names, references, prices, stock, statuses, booleans and tags. Column filters and sorting apply before pagination, and the result includes totals. It uses fictional in-memory records.

product-actions.ts
import type { TableActions } from "@/components/ui/yayaw-table-vue/types";
import { products } from "../shared/products";
import { listRecords } from "./list-records";

export const productActions: TableActions = {
  list: (input) => listRecords(products, input),
};
export const getTableActions = (tableType: string) =>
  tableType === "products" ? productActions : undefined;

Complete list adapter

list-records.ts
import {
  compatibleListParams,
  matchesContractFilter,
  normalizeFilterEnvelope,
  recordValue,
} from "@/components/ui/yayaw-table-vue/table-contracts";

export function listRecords<T extends Record<string, unknown>>(
  rows: T[],
  input: unknown
) {
  const params = compatibleListParams(recordValue(input));
  const page = Number(params.page);
  const limit = Number(params.limit);
  const search = String(params.search).toLowerCase();
  const filters = Object.entries(recordValue(params.filters));
  const advanced = normalizeFilterEnvelope(params.advancedFilters).filters;
  const matching = rows.filter((product) => {
    const row: Record<string, unknown> = product;
    const matchesSearch =
      !search ||
      ["name", "reference", "price", "stock", "status", "active", "tags"].some(
        (key) => {
          const value = product[key];
          return (
            value !== null &&
            value !== undefined &&
            String(value).toLowerCase().includes(search)
          );
        }
      );
    const matchesColumns = filters.every(([id, value]) =>
      matchesContractFilter(row[id], {
        type: typeof row[id] === "number" ? "number" : "text",
        operator: Array.isArray(value) ? "isAnyOf" : "contains",
        values: Array.isArray(value) ? value : [value],
      })
    );
    const matchesRule = (rule: Record<string, unknown>) =>
      matchesContractFilter(row[String(rule.columnId)], rule);
    const matchesAdvanced =
      advanced.length === 0 ||
      (params.advancedFilterJoin === "or"
        ? advanced.some(matchesRule)
        : advanced.every(matchesRule));
    return matchesSearch && matchesColumns && matchesAdvanced;
  });
  const sorting = Object.entries(recordValue(params.orderBy));
  matching.sort((left, right) => {
    for (const [id, direction] of sorting) {
      const a = (left as Record<string, unknown>)[id];
      const b = (right as Record<string, unknown>)[id];
      const comparison =
        typeof a === "number" && typeof b === "number"
          ? a - b
          : String(a ?? "").localeCompare(String(b ?? ""));
      if (comparison !== 0) {
        return direction === "desc" ? -comparison : comparison;
      }
    }
    return String(left.id).localeCompare(String(right.id));
  });
  return Promise.resolve({
    // Model a server response: later store mutations must not alter cached rows.
    data: structuredClone(matching.slice((page - 1) * limit, page * limit)),
    meta: {
      pageCount: Math.max(1, Math.ceil(matching.length / limit)),
      totalCount: matching.length,
    },
  });
}

4. Render the component

Expand ↗

Sample data. Changes stay in this preview.

5. Install the shared Query client

main.ts
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
import { createApp } from "vue";
import Products from "./products.vue";
import "@/components/ui/yayaw-table-vue/styles.css";

const app = createApp(Products);
app.use(VueQueryPlugin, { queryClient: new QueryClient() });
app.mount("#app");

Mount Products in an element with id="app", or use your application’s existing entry point and provider. Keep the registry styles in the application CSS entry. Vue does not use Nuqs.

Local data

For a local-only table, pass :data="products" and remove get-table-actions. Vue applies filtering, sorting, and pagination to that array. Keep remote lists action-driven; a single server page passed as local data cannot represent the complete dataset.

Nuxt and SSR

Install VueQueryPlugin in a Nuxt plugin with a new QueryClient per application/request. Reuse the project’s dehydration and hydration setup when preloading queries. Import the registry stylesheet from the Nuxt CSS configuration. Keep browser-only custom renderers behind the appropriate client boundary, and use initialData, initialRowCount, and initialPageCount only for the matching initial query.

Application notifications

Mount a single Toaster from vue-sonner at the application root and import vue-sonner/style.css when using the bare Sonner outlet. All table instances share it. Forms retain their inline validation and retry errors.

Continue with a capability

The rest of the documentation is organized by capability. Use the Vue tab in columns, forms, bulk actions, and views. The actions reference explains the request/result aliases shared with React.

Previous guide sections

Vue 3 and Nuxt

Read the current guide: concepts.

Feature coverage

Read the current guide: compatibility.

Runtime requirements

Read the current guide: compatibility.

Run the Vue workspace

Read the current guide: installation.

Migrate from TanStack Table 8

Read the current guide: concepts.

Register the package

Read the current guide: installation.

Configure a table

Read the current guide: configuration.

Table density

Read the current guide: layout.

Catalogue toolbar actions

Read the current guide: actions.

Connect server-backed actions

Read the current guide: actions.

Catalogue forms and submission lifecycle

Read the current guide: forms.

Choose forms by row

Read the current guide: forms.

Sections, translations, and custom controls

Read the current guide: form-layout.

Asynchronous relation options

Read the current guide: concepts.

Collections with isolated item editors

Read the current guide: collections.

Catalogue-backed inline editing

Read the current guide: inline-editing.

Share the Vue Query cache

Read the current guide: query-integration.

Build the shadcn-vue registry item

Read the current guide: installation.

React-to-Vue mapping

Read the current guide: compatibility.

Current React/Vue behavior

Read the current guide: concepts.

One column type across the interface

Read the current guide: concepts.

Structured generated forms

Read the current guide: form-layout.

Grouped rows

Read the current guide: grouping-calculations.

Card controls and empty results

Read the current guide: gallery.

Optional filter bar

Read the current guide: filtering.

Personal favorite views

Read the current guide: views.

Record consultation and activity

Read the current guide: record-details.