npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ioi-dev/vue-table

v0.2.6

Published

A performance-first Vue 3 data table component with a streamlined API surface and JavaScript-first defaults

Readme

@ioi-dev/vue-table

A performance-first Vue 3 data table component with a streamlined API surface and JavaScript-first defaults. Designed to deliver enterprise-grade performance without the complexity of larger alternatives.

v0.2.5 - Minimal CSS tier, row reorder, clipboard copy, column groups

Overview

IOI Vue Table provides a lightweight yet powerful solution for rendering large datasets in Vue 3 applications. It combines virtual scrolling, efficient sorting and filtering, and flexible customisation options whilst maintaining a small bundle footprint.

Key Features

  • Performance-First Architecture: Optimised for rendering thousands of rows with minimal overhead
  • Virtual Scrolling: Built-in virtualisation for smooth scrolling through large datasets
  • Dynamic Row Classes: Apply CSS classes per-row via string, object, or (row, index) => ... function
  • Auto-Size Columns: Programmatically size columns to fit content with a single call
  • Row Grouping: Group rows by single or multiple columns with aggregate calculations
  • Inline Editing: Cell-level editing with validation, keyboard commit/cancel, and Tab navigation
  • Row Selection: Single and multi-row selection with shift-click range and Ctrl+A
  • Flexible Column Definitions: Strongly-typed column configuration with support for various data types
  • Headless Pagination: Full control over pagination state with reactive bindings
  • Header Filters: Built-in support for text and select-based column filtering
  • Row Expansion: Expandable rows with custom content slots
  • CSV Export/Import: Secure data export with formula sanitisation and preview-based import
  • Server-Side Mode: Fetch data from server with debounced requests, loading/error states, cursor-based pagination, and infinite scroll support
  • Accessibility (a11y): Full keyboard navigation (Arrow keys, Home/End, PageUp/PageDown), focus management, ARIA attributes, live region announcements, and WCAG 2.1 AA compliance
  • TypeScript Support: Comprehensive type definitions with full generic inference
  • Zero-Dependency Core: Minimal external dependencies to reduce bundle size

Keywords

vue, vue3, vuejs, vue-3, table, datatable, data-table, grid, data-grid, table-component, vue-component, virtual-scroll, virtualization, virtual-list, sorting, filtering, pagination, grouping, row-groups, aggregations, group-by, row-expansion, csv-export, csv-import, typescript, ts, performance, lightweight, enterprise, responsive, headless, reactive, composition-api, vue-composition-api, frontend, ui-component, data-display, spreadsheet, ag-grid-alternative, tanstack-alternative

Installation

npm install @ioi-dev/vue-table

CSS Integration

The default package entry includes library CSS. For zero-CSS integration, use the unstyled entry point:

@ioi-dev/vue-table/unstyled

Available entry points:

| Entry Point | CSS | Use Case | |-------------|-----|----------| | @ioi-dev/vue-table | Full styles | Quick start | | @ioi-dev/vue-table/minimal | Functional-only (padding, borders, hover) | Custom design systems | | @ioi-dev/vue-table/unstyled | None | Headless / full CSS control |

Available CSS import paths:

  • Canonical: @ioi-dev/vue-table/styles.css
  • Compatibility alias: @ioi-dev/vue-table/style.css

The minimal CSS tier provides cell padding, row borders, sticky header, subtle hover, and keyboard focus ring — with zero brand colours, rounded corners, or shadows. Ideal as a starting point for custom themes:

import { Table } from '@ioi-dev/vue-table/unstyled';
import '@ioi-dev/vue-table/minimal';

Quick Start

<script setup lang="ts">
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface UserRow {
  id: number;
  name: string;
  score: number;
}

const columns: ColumnDef<UserRow>[] = [
  { field: 'id', header: 'ID', type: 'number', width: 90 },
  { field: 'name', header: 'Name', type: 'text' },
  { field: 'score', header: 'Score', type: 'number' }
];

const rows: UserRow[] = [
  { id: 1, name: 'Alpha', score: 91 },
  { id: 2, name: 'Beta', score: 77 }
];
</script>

<template>
  <Table :rows="rows" :columns="columns" row-key="id" :height="320" />
</template>

Note: IoiTable remains available as a backward-compatible alias for Table.

Advanced Usage

Pagination with Header Filters

This example demonstrates headless pagination with reactive state management and built-in header filters:

<script setup lang="ts">
import { ref } from 'vue';
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface UserRow {
  id: number;
  name: string;
  status: string;
}

const rows = ref<UserRow[]>([]);
const pageIndex = ref(0);
const pageSize = ref(25);

const columns: ColumnDef<UserRow>[] = [
  { field: 'id', header: 'ID' },
  { field: 'status', header: 'Status', headerFilter: 'select' },
  { field: 'name', header: 'Name', headerFilter: 'text' }
];
</script>

<template>
  <Table
    v-model:pageIndex="pageIndex"
    v-model:pageSize="pageSize"
    :rows="rows"
    :columns="columns"
    row-key="id"
  />
</template>

Row Grouping

Group rows by single or multiple columns with aggregate calculations:

<script setup lang="ts">
import { ref } from 'vue';
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface SalesRow {
  id: number;
  region: string;
  product: string;
  amount: number;
}

const rows = ref<SalesRow[]>([
  { id: 1, region: 'North', product: 'Widget', amount: 100 },
  { id: 2, region: 'North', product: 'Gadget', amount: 200 },
  { id: 3, region: 'South', product: 'Widget', amount: 150 },
  { id: 4, region: 'South', product: 'Gadget', amount: 250 }
]);

const expandedGroups = ref<string[]>(['North']);

const groupAggregations = {
  amount: ['sum', 'avg']
};
</script>

<template>
  <Table
    v-model:expandedGroupKeys="expandedGroups"
    :rows="rows"
    :columns="[
      { field: 'region', header: 'Region' },
      { field: 'product', header: 'Product' },
      { field: 'amount', header: 'Amount', type: 'number' }
    ]"
    row-key="id"
    group-by="region"
    :group-aggregations="groupAggregations"
  />
</template>

Server-Side Mode

For large datasets or real-time data, use server-side mode to fetch data on demand:

<script setup lang="ts">
import { ref } from 'vue';
import { Table, type ServerDataOptions, type ColumnDef } from '@ioi-dev/vue-table';

interface UserRow {
  id: number;
  name: string;
  email: string;
}

const columns: ColumnDef<UserRow>[] = [
  { field: 'id', header: 'ID', type: 'number' },
  { field: 'name', header: 'Name', type: 'text' },
  { field: 'email', header: 'Email', type: 'text' }
];

const serverOptions: ServerDataOptions<UserRow> = {
  query: async (params) => {
    const response = await fetch(`/api/users?page=${params.pageIndex}&size=${params.pageSize}`);
    const data = await response.json();
    return {
      rows: data.items,
      totalRows: data.total
    };
  },
  debounceMs: 300
};
</script>

<template>
  <Table
    data-mode="server"
    :server-options="serverOptions"
    :columns="columns"
    row-key="id"
  />
</template>

Row Selection

Single and multi-row selection with reactive state:

<script setup lang="ts">
import { ref } from 'vue';
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface UserRow { id: number; name: string; role: string; }

const columns: ColumnDef<UserRow>[] = [
  { field: 'name', header: 'Name' },
  { field: 'role', header: 'Role' }
];

const rows: UserRow[] = [
  { id: 1, name: 'Alice', role: 'Admin' },
  { id: 2, name: 'Bob', role: 'Editor' }
];

const selectedKeys = ref<number[]>([]);
</script>

<template>
  <Table
    v-model:selectedRowKeys="selectedKeys"
    :rows="rows"
    :columns="columns"
    row-key="id"
    selection-mode="multi"
  />
  <p>Selected: {{ selectedKeys }}</p>
</template>

Inline Editing

Enable cell editing per column. Commit with Enter, cancel with Escape, Tab to move between cells:

<script setup lang="ts">
import { ref } from 'vue';
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface ProductRow { id: number; name: string; price: number; }

const rows = ref<ProductRow[]>([
  { id: 1, name: 'Widget', price: 9.99 },
  { id: 2, name: 'Gadget', price: 24.99 }
]);

const columns: ColumnDef<ProductRow>[] = [
  { field: 'id', header: 'ID', editable: false },
  { field: 'name', header: 'Name' },
  { field: 'price', header: 'Price', type: 'number' }
];

function onCellEdit(payload: { field: string; rowIndex: number; oldValue: unknown; newValue: unknown }) {
  const row = rows.value[payload.rowIndex];
  (row as Record<string, unknown>)[payload.field] = payload.newValue;
}
</script>

<template>
  <Table
    :rows="rows"
    :columns="columns"
    row-key="id"
    @cell-edit-commit="onCellEdit"
  />
</template>

All columns are editable by default. Set editable: false on a column to opt out.

Dynamic Row Classes

Apply CSS classes to rows based on data using a string, object, or function:

<script setup lang="ts">
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface OrderRow { id: number; status: 'active' | 'pending' | 'error'; total: number; }

const columns: ColumnDef<OrderRow>[] = [
  { field: 'id', header: 'ID' },
  { field: 'status', header: 'Status' },
  { field: 'total', header: 'Total', type: 'number' }
];

const rows: OrderRow[] = [
  { id: 1, status: 'active', total: 120 },
  { id: 2, status: 'pending', total: 45 },
  { id: 3, status: 'error', total: 0 }
];

function getRowClass(row: OrderRow): Record<string, boolean> {
  return {
    'row--active': row.status === 'active',
    'row--pending': row.status === 'pending',
    'row--error': row.status === 'error'
  };
}
</script>

<template>
  <Table :rows="rows" :columns="columns" row-key="id" :row-class="getRowClass" />
</template>

<style>
.row--active { background: #f0fdf4; }
.row--pending { background: #fffbeb; }
.row--error  { background: #fef2f2; }
</style>

Auto-Size Columns

Resize columns to fit their content based on rendered cell widths:

<script setup lang="ts">
import { ref } from 'vue';
import { Table } from '@ioi-dev/vue-table';

const tableRef = ref<InstanceType<typeof Table> | null>(null);

function fitAll() {
  tableRef.value?.autoSizeColumns();
}

function fitNameOnly() {
  tableRef.value?.autoSizeColumns(['name'], { maxWidth: 300 });
}
</script>

<template>
  <button @click="fitAll">Fit All</button>
  <button @click="fitNameOnly">Fit Name</button>
  <Table ref="tableRef" :rows="rows" :columns="columns" row-key="id" />
</template>

autoSizeColumns(columnIds?, options?) accepts an optional list of column IDs and an options object:

| Option | Default | Description | |--------|---------|-------------| | includeHeaders | true | Include header cell widths in the calculation | | padding | 16 | Extra padding added to each measured cell | | minWidth | 50 | Floor width in pixels | | maxWidth | 500 | Ceiling width in pixels |

Accessibility

The table is built with accessibility in mind:

  • Keyboard Navigation: Full support for Arrow keys, Home/End, Page Up/Down
  • Focus Management: Visible focus indicators and focus trapping in edit mode
  • Screen Reader Support: ARIA attributes and live region announcements
  • WCAG 2.1 AA Compliance: Color contrast, focus visibility, and reduced motion support
<template>
  <Table
    :rows="rows"
    :columns="columns"
    row-key="id"
    aria-label="Employee directory"
  />
</template>

Row Reorder

Enable drag-and-drop row reordering. The table does not mutate your data — it fires a row-reorder event with the source and destination indices so you can update your data source:

<script setup lang="ts">
import { ref } from 'vue';
import { Table, type ColumnDef } from '@ioi-dev/vue-table';

interface TaskRow { id: number; title: string; priority: number; }

const rows = ref<TaskRow[]>([
  { id: 1, title: 'Design mockups', priority: 1 },
  { id: 2, title: 'Implement API', priority: 2 },
  { id: 3, title: 'Write tests', priority: 3 }
]);

const columns: ColumnDef<TaskRow>[] = [
  { field: 'id', header: 'ID', type: 'number' },
  { field: 'title', header: 'Title' },
  { field: 'priority', header: 'Priority', type: 'number' }
];

function onReorder(payload: { fromIndex: number; toIndex: number }) {
  const [moved] = rows.value.splice(payload.fromIndex, 1);
  rows.value.splice(payload.toIndex, 0, moved);
}
</script>

<template>
  <Table
    :rows="rows"
    :columns="columns"
    row-key="id"
    row-draggable
    @row-reorder="onReorder"
  />
</template>

Drag handles are keyboard-accessible: use Alt+Arrow keys to move the focused row up or down.

Clipboard Copy

When row selection is active, pressing Ctrl+C (Cmd+C on macOS) copies selected rows as tab-separated values to the clipboard. Headers are included and hidden columns are excluded.

copyable?: boolean  // default: true when selection is enabled

You can also call the method programmatically via the table ref:

<script setup lang="ts">
import { ref } from 'vue';
import { Table } from '@ioi-dev/vue-table';

const tableRef = ref<InstanceType<typeof Table> | null>(null);

async function copySelected() {
  await tableRef.value?.copySelectionToClipboard();
}
</script>

<template>
  <button @click="copySelected">Copy Selection</button>
  <Table ref="tableRef" :rows="rows" :columns="columns" row-key="id" selection-mode="multi" />
</template>

Column Groups

Render spanning multi-level column headers by grouping columns under shared header cells:

<script setup lang="ts">
import { Table, type ColumnDef, type ColumnGroup } from '@ioi-dev/vue-table';

interface SalesRow { id: number; q1: number; q2: number; q3: number; q4: number; region: string; }

const columns: ColumnDef<SalesRow>[] = [
  { id: 'region', field: 'region', header: 'Region' },
  { id: 'q1', field: 'q1', header: 'Q1', type: 'number' },
  { id: 'q2', field: 'q2', header: 'Q2', type: 'number' },
  { id: 'q3', field: 'q3', header: 'Q3', type: 'number' },
  { id: 'q4', field: 'q4', header: 'Q4', type: 'number' }
];

const columnGroups: ColumnGroup[] = [
  { id: 'quarters', header: 'Quarterly Revenue', columnIds: ['q1', 'q2', 'q3', 'q4'] }
];
</script>

<template>
  <Table :rows="rows" :columns="columns" :column-groups="columnGroups" row-key="id" />
</template>

Column groups support a #column-group-header slot for custom group header content. Single-level only in v0.2.5; nested groups are planned for a future release.

Configuration Options

Behaviour Defaults

| Option | Default | Description | |--------|---------|-------------| | sanitizeFormulas | true | Sanitises formula-like prefixes in CSV exports to prevent injection attacks | | globalSearchDebounceMs | 0 | Debounce interval for global search input (milliseconds) | | filterDebounceMs | 0 | Debounce interval for filter operations (milliseconds) | | rowHeight | Configurable | Height of each row for virtualisation calculations | | overscan | Configurable | Number of extra rows to render outside viewport for smoother scrolling |

Documentation

Requirements

  • Vue 3.4 or higher
  • Modern browser with ES2020 support

License

MIT

Contributing

Contributions are welcome. Please refer to the repository guidelines for submission requirements and coding standards.