Skip to content

Table

Data table component with sorting, custom columns, cell formatting, and row customization. Supports dynamic slot-based cell rendering and sortable columns.

Import

typescript
import { Table } from 'vuiii'

Basic Usage

Name
Email
Charliecharlie@example.com
Alicealice@example.com
Bobbob@example.com
vue
<script setup>
import { Table } from 'vuiii'
import type { TableColumn } from 'vuiii'

type User = { id: number; name: string; email: string }

const columns: TableColumn<User>[] = [
  { name: 'name', label: 'Name' },
  { name: 'email', label: 'Email' },
]
</script>

<template>
  <Table :items="users" :columns="columns" />
</template>

A column reads item[name] by default. Give it a value function to derive the cell instead, and a formatter to control how it is displayed.

Props

PropTypeDefaultDescription
itemsT[]-Array of row items to render
columnsTableColumn<T>[]-Column definitions
rowClassstring | ((row: { item, index }) => any)-Class applied to each row (static or per-row)
highlightOnHoverbooleanfalseHighlights rows on hover
noDataMessagestring-Message shown when items is empty
size'small' | 'normal' | 'large''normal'Row/cell density (padding + font size)
sortColumnNamestring | nullnullCurrently sorted column (use with v-model:sort-column-name)
sortDirection'asc' | 'desc''asc'Sort direction (use with v-model:sort-direction)

Slots

SlotDescription
column:{name}Custom cell content for a column. Props: { item, value, index, column }
header:{name}Custom header content for a column. Props: { column }
rowOptionsActions displayed at the end of each row. Props: { item, index }
noDataMessageCustom content when items is empty
toolsAdditional header row tools (adds an extra th column)

Events

EventPayloadDescription
click-row{ item: T, index: number }When a row is clicked
mouseenter-row{ item: T, index: number }When the mouse enters a row
mouseleave-row{ item: T, index: number }When the mouse leaves a row
sort{ sortColumnName: string, sortDirection: 'asc' | 'desc' }When sort column or direction changes

Custom Cells

column:{name} replaces a single column's cells. The slot name does not have to match an existing data property — a column named status with no matching field is a perfectly good place to render something derived.

Name
Status
Charlie Active
Alice Inactive
Bob Active
vue
<Table :items="users" :columns="columns">
  <template #column:name="{ value }">
    <strong>{{ value }}</strong>
  </template>
  <template #column:status="{ item }">
    <Badge :color="item.active ? 'success' : 'danger'">
      {{ item.active ? 'Active' : 'Inactive' }}
    </Badge>
  </template>
</Table>

Row Actions

rowOptions adds a trailing column of per-row actions. The tools slot fills its header cell.

Name
Email
Actions
Charliecharlie@example.com
Alicealice@example.com
Bobbob@example.com
vue
<Table :items="users" :columns="columns">
  <template #tools>Actions</template>
  <template #rowOptions="{ item }">
    <IconButton icon="pencil" title="Edit" @click="edit(item)" />
    <IconButton icon="trash" color="danger" title="Delete" @click="remove(item)" />
  </template>
</Table>

Clicks inside that cell do not bubble up as click-row, so row actions and a clickable row can coexist.

Sorting

Mark a column sortable and the header becomes a button. The sort state is yours to hold, through v-model:sort-column-name and v-model:sort-direction — which means the table can either sort the rows it was given, or you can ignore its rows and refetch from a server.

Name
Signed up
Alice14/11/2023
Bob22/01/2025
Charlie01/03/2024
Sorted by name (asc)
vue
<Table
  v-model:sort-column-name="sortColumn"
  v-model:sort-direction="sortDir"
  :items="users"
  :columns="[
    { name: 'name', label: 'Name', sortable: true },
    { name: 'signedUp', label: 'Signed up', sortable: true },
  ]"
/>

Values are compared as strings or numbers depending on their type, and empty cells are grouped at the end. Pass a sorter on the column for anything else:

ts
{ name: 'priority', label: 'Priority', sortable: true, sorter: (a, b) => order[a] - order[b] }

Row Interaction

Name
Email
Charliecharlie@example.com
Alicealice@example.com
Bobbob@example.com
Click a row
vue
<Table
  :items="users"
  :columns="columns"
  highlight-on-hover
  @click-row="({ item }) => open(item)"
/>

rowClass accepts a function too, for styling rows by their data:

vue
<Table :items="users" :columns="columns" :row-class="({ item }) => (item.active ? '' : 'is-muted')" />

Empty State

Name
Email
No users yet
vue
<Table :items="[]" :columns="columns" no-data-message="No users yet" />

Use the noDataMessage slot for richer content — an illustration, or a button that creates the first record.

Density

Name
Email
Charliecharlie@example.com
Alicealice@example.com
Name
Email
Charliecharlie@example.com
Alicealice@example.com
vue
<Table :items="users" :columns="columns" size="small" />
<Table :items="users" :columns="columns" size="large" />

Linked Cells

Give a column an href and its cells render as router links.

ts
const columns: TableColumn<User>[] = [
  { name: 'name', label: 'Name', href: (user) => ({ name: 'user', params: { id: user.id } }) },
]

Storybook

For interactive examples with all variants, see Table in Storybook.

Released under the MIT License.