Table
Data-driven table component. Pass columns and data as JSON, customize cells with scoped slots.
Basic
<script setup>
import { VxTable } from 'veloxui'
// Define table columns — each column needs a unique "key" matching a field in the data
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
{ key: 'joined', label: 'Joined' },
]
// Row data — each object should have keys matching the column "key" values
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active', joined: 'Jan 4, 2024' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active', joined: 'Feb 12, 2024' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive', joined: 'Mar 8, 2024' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active', joined: 'Apr 22, 2024' },
{ name: 'Sofia Davis', email: 'sofia.davis@email.com', role: 'Admin', status: 'Pending', joined: 'May 15, 2024' },
]
</script>
<template>
<!-- The simplest usage: just pass columns and data -->
<VxTable :columns="columns" :data="data" />
<!-- You can customize any cell using a slot named after the column key -->
<VxTable :columns="columns" :data="data">
<!-- Slot props: row = full row object, value = cell value, index = row index -->
<template #name="{ row }">
<strong>{{ row.name }}</strong>
</template>
<template #email="{ value }">
<span style="font-family: monospace; font-size: 13px;">{{ value }}</span>
</template>
<template #status="{ value }">
<span :style="{ color: value === 'Active' ? '#22c55e' : '#a1a1aa' }">
{{ value }}
</span>
</template>
</VxTable>
</template>Render Function
Use the render property on a column to return HTML directly — no slots needed. Great for quick formatting without template boilerplate.
<script setup>
import { VxTable } from 'veloxui'
// The "render" property returns an HTML string — no slot needed
const columns = [
{
key: 'name',
label: 'Name',
render: (row) => `<strong>${row.name}</strong>`,
},
{
key: 'status',
label: 'Status',
render: (row) => {
const colors = { Active: '#22c55e', Inactive: '#a1a1aa', Pending: '#f59e0b' }
const color = colors[row.status] || '#a1a1aa'
return `<span style="color:${color}">${row.status}</span>`
},
},
{ key: 'email', label: 'Email' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', status: 'Pending' },
]
</script>
<template>
<VxTable :columns="columns" :data="data" bordered />
</template>Bordered
Add the bordered prop for outer border, rounded corners, and column dividers.
<script setup>
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
]
</script>
<template>
<!-- "bordered" adds outer border, rounded corners, and column dividers -->
<VxTable :columns="columns" :data="data" bordered />
</template>Striped
Alternate row backgrounds with the striped prop.
<script setup>
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
{ name: 'Sofia Davis', email: 'sofia.davis@email.com', role: 'Admin', status: 'Pending' },
]
</script>
<template>
<!-- "striped" alternates row background colors for readability -->
<VxTable :columns="columns" :data="data" striped bordered />
</template>Fixed Header
Set max-height to make the body scroll while the header stays fixed automatically.
<script setup>
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
// When there are many rows, use max-height to make the body scrollable
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
{ name: 'Sofia Davis', email: 'sofia.davis@email.com', role: 'Admin', status: 'Pending' },
{ name: 'Emma Wilson', email: 'emma.wilson@email.com', role: 'Editor', status: 'Active' },
{ name: 'Noah Brown', email: 'noah.brown@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'Ava Garcia', email: 'ava.garcia@email.com', role: 'Admin', status: 'Active' },
{ name: 'Ethan Miller', email: 'ethan.m@email.com', role: 'Editor', status: 'Pending' },
{ name: 'Mia Anderson', email: 'mia.anderson@email.com', role: 'Viewer', status: 'Active' },
{ name: 'Lucas Taylor', email: 'lucas.t@email.com', role: 'Admin', status: 'Active' },
{ name: 'Charlotte Thomas', email: 'charlotte.t@email.com', role: 'Editor', status: 'Inactive' },
]
</script>
<template>
<!-- max-height keeps the header fixed while the body scrolls -->
<VxTable :columns="columns" :data="data" :max-height="400" bordered />
</template>Fixed Columns
Set fixed: 'left' or fixed: 'right' on a column to pin it while the rest scrolls horizontally. Requires width on fixed columns.
<script setup>
import { VxTable } from 'veloxui'
// Use "fixed" to pin columns. Requires "width" on fixed columns.
const columns = [
{ key: 'name', label: 'Name', fixed: 'left', width: 180 },
{ key: 'email', label: 'Email', width: 250 },
{ key: 'role', label: 'Role', width: 150 },
{ key: 'department', label: 'Department', width: 180 },
{ key: 'location', label: 'Location', width: 180 },
{ key: 'status', label: 'Status', width: 150 },
{ key: 'joined', label: 'Joined', width: 150 },
{ key: 'actions', label: '', fixed: 'right', width: 80 },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', department: 'Engineering', location: 'San Francisco', status: 'Active', joined: 'Jan 4, 2024' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', department: 'Marketing', location: 'New York', status: 'Active', joined: 'Feb 12, 2024' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', department: 'Design', location: 'London', status: 'Inactive', joined: 'Mar 8, 2024' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', department: 'Engineering', location: 'Tokyo', status: 'Active', joined: 'Apr 22, 2024' },
]
</script>
<template>
<!-- "Name" stays pinned on the left, "actions" on the right, rest scrolls -->
<VxTable :columns="columns" :data="data" bordered :max-height="350">
<template #actions>
<button style="background: none; border: none; cursor: pointer;">···</button>
</template>
</VxTable>
</template>Selection
Add selectable to show checkboxes. Use v-model:selected to bind selected values. By default it uses the id field — set selection-key to change it.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const selected = ref([])
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor' },
]
</script>
<template>
<!-- Show which rows are selected -->
<p>Selected: {{ selected.length ? selected.join(', ') : 'none' }}</p>
<!-- "selectable" adds checkboxes, "selection-key" sets which field is used as the value -->
<VxTable
:columns="columns"
:data="data"
selectable
v-model:selected="selected"
selection-key="name"
bordered
/>
<!-- selected = ['Olivia Martin', 'Jackson Lee'] when those rows are checked -->
</template>Drag & Drop
Add draggable to enable row reordering. Use v-model:data to get the updated order.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
// Data must be a ref so the table can update the order after drag
const data = ref([
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
{ name: 'Sofia Davis', email: 'sofia.davis@email.com', role: 'Admin', status: 'Pending' },
])
</script>
<template>
<!-- "draggable" enables row reordering by drag & drop -->
<!-- v-model:data syncs the new order back to your data ref -->
<VxTable :columns="columns" v-model:data="data" draggable bordered />
</template>Resizable Columns
Add the resizable prop to allow users to drag column borders to resize. You can also set resizable per column in the column definition.
<script setup>
import { VxTable } from 'veloxui'
// Set "width" on columns to define initial widths for resizing
const columns = [
{ key: 'name', label: 'Name', width: 200 },
{ key: 'email', label: 'Email', width: 260 },
{ key: 'role', label: 'Role', width: 140 },
{ key: 'status', label: 'Status', width: 140 },
{ key: 'joined', label: 'Joined', width: 160 },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active', joined: 'Jan 4, 2024' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active', joined: 'Feb 12, 2024' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive', joined: 'Mar 8, 2024' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active', joined: 'Apr 22, 2024' },
]
</script>
<template>
<!-- Object form: set min/max width limits for all columns -->
<VxTable :columns="columns" :data="data" :resizable="{ minWidth: 80, maxWidth: 500 }" bordered />
<!-- Boolean form: enable resizing with default limits (minWidth: 50) -->
<VxTable :columns="columns" :data="data" resizable bordered />
</template>Sorting
Add sortable to enable clickable column headers. Click cycles through ascending, descending, and unsorted. Use v-model:sort for controlled sorting, or let the table manage it internally.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
]
// Optional: control sort state externally
const sort = ref({ key: 'name', order: 'asc' })
</script>
<template>
<!-- "sortable" makes all column headers clickable to sort -->
<VxTable :columns="columns" :data="data" sortable bordered />
<!-- Controlled: bind sort state with v-model:sort -->
<VxTable :columns="columns" :data="data" sortable v-model:sort="sort" bordered />
<!-- Per-column: only make specific columns sortable -->
<VxTable
:columns="[
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email' },
]"
:data="data"
bordered
/>
</template>Loading
Add the loading prop to show a spinner overlay.
<script setup>
import { ref } from 'vue'
import { VxTable, VxButton } from 'veloxui'
const loading = ref(false)
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor' },
]
// Simulate a loading state
function toggleLoading() {
loading.value = true
setTimeout(() => { loading.value = false }, 2000)
}
</script>
<template>
<VxButton size="sm" variant="outline" @click="toggleLoading">Toggle loading</VxButton>
<!-- "loading" shows a spinner overlay on top of the table -->
<VxTable :columns="columns" :data="data" :loading="loading" bordered />
</template>Expandable Rows
Add expandable to show an expand toggle on each row. Provide the expand slot for the detail content. Uses selection-key (default id) to track expanded rows.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', joined: 'Jan 4, 2024' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', joined: 'Feb 12, 2024' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', joined: 'Mar 8, 2024' },
]
// Optional: track which rows are expanded externally
const expanded = ref([])
</script>
<template>
<!-- "expandable" adds an expand toggle to each row -->
<!-- "selection-key" determines which field uniquely identifies each row -->
<VxTable :columns="columns" :data="data" expandable selection-key="name" bordered>
<!-- The "expand" slot renders content below the row when expanded -->
<template #expand="{ row }">
<div>
<strong>{{ row.name }}</strong> joined on {{ row.joined }} as {{ row.role }}.
<br />Contact: {{ row.email }}
</div>
</template>
</VxTable>
<!-- Controlled: track expanded rows with v-model:expanded -->
<VxTable
:columns="columns"
:data="data"
expandable
v-model:expanded="expanded"
selection-key="name"
bordered
>
<template #expand="{ row }">
<div>Details for {{ row.name }}</div>
</template>
</VxTable>
</template>Pagination
Add pagination to show page controls. Pass true for built-in state, or an object for controlled pagination.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
{ name: 'Sofia Davis', email: 'sofia.davis@email.com', role: 'Admin', status: 'Pending' },
{ name: 'Emma Wilson', email: 'emma.wilson@email.com', role: 'Editor', status: 'Active' },
{ name: 'Noah Brown', email: 'noah.brown@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'Ava Garcia', email: 'ava.garcia@email.com', role: 'Admin', status: 'Active' },
]
// Optional: control pagination state (page, pageSize, available sizes)
const page = ref({ page: 1, pageSize: 5, pageSizes: [5, 10, 25] })
</script>
<template>
<!-- "pagination" as boolean: uses built-in state, defaults to 10 per page -->
<VxTable :columns="columns" :data="data" pagination bordered />
<!-- Controlled: bind pagination state with v-model:pagination -->
<VxTable :columns="columns" :data="data" v-model:pagination="page" bordered />
</template>Global Filter
Pass a filter string to search across all visible columns.
<script setup>
import { ref } from 'vue'
import { VxTable, VxInput } from 'veloxui'
const filterText = ref('')
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
]
</script>
<template>
<!-- Use VxInput to capture the search text -->
<VxInput v-model="filterText" placeholder="Search..." size="sm" style="max-width: 260px;" />
<!-- "filter" searches across all visible columns and hides non-matching rows -->
<VxTable :columns="columns" :data="data" :filter="filterText" bordered />
</template>Row Grouping
Set group-by to a column key to group rows with collapsible headers.
<script setup>
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
{ name: 'Sofia Davis', email: 'sofia.davis@email.com', role: 'Admin', status: 'Pending' },
]
</script>
<template>
<!-- "group-by" groups rows by the value of the specified column key -->
<!-- Rows with the same "role" will be grouped under collapsible headers -->
<VxTable :columns="columns" :data="data" group-by="role" bordered />
</template>Column Visibility
Add the column-toggle prop to show a built-in column visibility dropdown. The dropdown is auto-populated from the columns definition using a multi-select.
<script setup>
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
{ key: 'joined', label: 'Joined' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active', joined: 'Jan 4, 2024' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active', joined: 'Feb 12, 2024' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive', joined: 'Mar 8, 2024' },
]
</script>
<template>
<!-- "column-toggle" adds a dropdown to show/hide columns -->
<VxTable :columns="columns" :data="data" column-toggle bordered />
</template>Column Reorder
Pass v-model:column-order to enable drag-and-drop column reordering.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
{ key: 'status', label: 'Status' },
{ key: 'joined', label: 'Joined' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active', joined: 'Jan 4, 2024' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active', joined: 'Feb 12, 2024' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive', joined: 'Mar 8, 2024' },
]
// Column order as an array of column keys — drag headers to reorder
const order = ref(['name', 'email', 'role', 'status', 'joined'])
</script>
<template>
<!-- v-model:column-order enables drag-and-drop column reordering -->
<VxTable :columns="columns" :data="data" v-model:column-order="order" bordered />
</template>Inline Editing
Set editable: true on a column to enable double-click-to-edit. Listen to @cell-edit to update the data.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
// Set "editable: true" on columns you want to be editable
const columns = [
{ key: 'name', label: 'Name', editable: true },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role', editable: true },
{ key: 'status', label: 'Status' },
]
// Each row needs a unique "id" field (used internally to identify the row)
const data = ref([
{ id: 1, name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin', status: 'Active' },
{ id: 2, name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor', status: 'Active' },
{ id: 3, name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer', status: 'Inactive' },
{ id: 4, name: 'William Kim', email: 'will@email.com', role: 'Editor', status: 'Active' },
])
// Handle cell edits — update the data when a cell value changes
function onCellEdit(key, row, value) {
const item = data.value.find(r => r.id === row.id)
if (item) item[key] = value
}
</script>
<template>
<!-- Double-click an editable cell to edit it -->
<!-- Press Enter to save, Escape to cancel -->
<VxTable :columns="columns" :data="data" @cell-edit="onCellEdit" bordered />
</template>Remote Data
Pass a fetch-data function to load data with infinite scroll — just like the select dropdown. The table fetches the first page on mount, then loads more rows as you scroll to the bottom. Re-fetches from page 1 when sort or filter changes.
<script setup>
import { VxTable } from 'veloxui'
const columns = [
{ key: 'id', label: 'ID', width: 80 },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
]
// "fetchData" receives { page, pageSize, sort, filter }
// Must return { data: [...rows], total: totalRowCount }
// The table calls this automatically:
// - On mount (page 1)
// - When scrolling near the bottom (next page, appended)
// - When sort or filter changes (resets to page 1)
async function fetchUsers({ page, pageSize }) {
const res = await fetch(`/api/users?page=${page}&limit=${pageSize}`)
const json = await res.json()
return { data: json.items, total: json.total }
}
</script>
<template>
<!-- "fetch-data" enables server-side data with infinite scroll -->
<!-- Set "max-height" to make the table scrollable -->
<VxTable
:columns="columns"
:fetch-data="fetchUsers"
:max-height="350"
sortable
bordered
/>
</template>Row Click
Listen to the @row-click event to handle row clicks.
<script setup>
import { ref } from 'vue'
import { VxTable } from 'veloxui'
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
]
const data = [
{ name: 'Olivia Martin', email: 'olivia.martin@email.com', role: 'Admin' },
{ name: 'Jackson Lee', email: 'jackson.lee@email.com', role: 'Editor' },
{ name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', role: 'Viewer' },
]
const clickedRow = ref(null)
function handleRowClick(row, index) {
clickedRow.value = row
console.log('Clicked row:', row, 'at index:', index)
}
</script>
<template>
<p v-if="clickedRow">Clicked: {{ clickedRow.name }}</p>
<!-- @row-click fires when any row is clicked -->
<VxTable :columns="columns" :data="data" @row-click="handleRowClick" bordered />
</template>Summary Row
Use the footer prop to add a summary/totals row. Footer cells support the same footer-[key] slots for custom content.
<script setup>
import { VxTable } from 'veloxui'
// Use "align" to right-align numeric columns
const columns = [
{ key: 'product', label: 'Product' },
{ key: 'qty', label: 'Qty', align: 'right' },
{ key: 'price', label: 'Price', align: 'right' },
{ key: 'total', label: 'Total', align: 'right' },
]
const data = [
{ product: 'MacBook Pro 14"', qty: 2, price: '$1,999.00', total: '$3,998.00' },
{ product: 'AirPods Pro', qty: 5, price: '$249.00', total: '$1,245.00' },
{ product: 'Magic Keyboard', qty: 3, price: '$299.00', total: '$897.00' },
{ product: 'Studio Display', qty: 1, price: '$1,599.00', total: '$1,599.00' },
{ product: 'iPhone 15 Pro', qty: 4, price: '$999.00', total: '$3,996.00' },
]
// "footer" has the same shape as data — it renders as a sticky bottom row
const footer = [
{ product: 'Total (5 items)', qty: 15, price: '', total: '$11,735.00' },
]
</script>
<template>
<VxTable :columns="columns" :data="data" :footer="footer" bordered>
<!-- Customize footer cells with "footer-[key]" slots -->
<template #footer-product="{ value }">
<span style="font-weight: 600;">{{ value }}</span>
</template>
<template #footer-total="{ value }">
<span style="font-weight: 700;">{{ value }}</span>
</template>
</VxTable>
</template>API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | TableColumn[] | — | Column definitions (required) |
data | Record<string, any>[] | [] | Row data array |
footer | Record<string, any>[] | — | Footer row data (same shape as data) |
bordered | boolean | false | Outer border, rounded corners, column dividers |
striped | boolean | false | Alternate row backgrounds |
height | string | number | — | Fixed height with sticky header |
maxHeight | string | number | — | Max height with sticky header |
caption | string | — | Table caption text |
scrollbar | 'auto' | 'hover' | 'always' | 'hidden' | 'hover' | Scrollbar visibility |
selectable | boolean | false | Show row checkboxes |
selected | any[] | — | Selected values (v-model) |
selectionKey | string | 'id' | Row field used as selection/expand key |
draggable | boolean | false | Enable drag & drop row reordering |
resizable | boolean | TableResizable | false | Enable column resizing |
sortable | boolean | false | Enable column sorting |
sort | TableSort | null | — | Sort state (v-model). If omitted, uses internal state |
loading | boolean | false | Show loading spinner overlay |
expandable | boolean | false | Show expand toggle per row |
expanded | any[] | — | Expanded row keys (v-model). If omitted, uses internal state |
pagination | boolean | TablePagination | false | Enable pagination. true for built-in, object for controlled |
filter | string | '' | Global text filter across all visible columns |
groupBy | string | — | Column key to group rows by |
columnToggle | boolean | false | Show built-in column visibility dropdown |
columnOrder | string[] | — | Column key order (v-model). Enables drag reorder |
fetchData | (params: TableFetchParams) => Promise<TableFetchResult> | — | Server-side data with infinite scroll. Loads more on scroll, re-fetches on sort/filter change |
pageCount | number | 0 | Max number of pages to fetch. 0 means unlimited (stops when data.length >= total) |
scrollDistance | number | 50 | Distance from bottom (px) to trigger loading more |
TableColumn
| Property | Type | Default | Description |
|---|---|---|---|
key | string | — | Data field key (required) |
label | string | — | Header text (required) |
width | string | number | — | Column width |
align | 'left' | 'center' | 'right' | 'left' | Text alignment |
fixed | 'left' | 'right' | — | Pin column to left or right edge |
resizable | boolean | TableResizable | — | Override table-level resizable per column |
sortable | boolean | — | Override table-level sortable per column |
editable | boolean | — | Enable double-click inline editing |
render | (row, index) => string | — | HTML render function for cell content |
TableResizable
| Property | Type | Default | Description |
|---|---|---|---|
minWidth | number | 50 | Minimum width (px) |
maxWidth | number | Infinity | Maximum width (px) |
TableSort
| Property | Type | Description |
|---|---|---|
key | string | Column key being sorted |
order | 'asc' | 'desc' | Sort direction |
TablePagination
| Property | Type | Default | Description |
|---|---|---|---|
page | number | — | Current page (1-based) |
pageSize | number | — | Rows per page |
total | number | — | Total item count (for server-side pagination) |
pageSizes | number[] | [10, 20, 50, 100] | Page size options |
TableFetchParams
| Property | Type | Description |
|---|---|---|
page | number | Current page (1-based) |
pageSize | number | Rows per page |
sort | TableSort | null | Current sort state |
filter | string | Current filter text |
TableFetchResult
| Property | Type | Description |
|---|---|---|
data | Record<string, any>[] | Rows for the current page |
total | number | Total item count (all pages) |
Slots
| Slot | Props | Description |
|---|---|---|
[column.key] | { row, value, index } | Custom cell content |
header-[column.key] | { column } | Custom header cell |
footer-[column.key] | { row, value, index } | Custom footer cell |
expand | { row, index } | Expandable row detail content |
expand-icon | { row, expanded } | Custom expand toggle icon |
group | { group, count } | Custom group header content |
caption | — | Custom caption |
empty | — | Custom empty state |
Events
| Event | Payload | Description |
|---|---|---|
update:selected | any[] | Selection changed |
update:data | Record<string, any>[] | Row order changed (drag) |
update:sort | TableSort | null | Sort state changed |
update:expanded | any[] | Expanded rows changed |
update:pagination | TablePagination | Page/pageSize changed |
update:columnOrder | string[] | Column order changed (drag) |
row-click | (row, index) | Row clicked |
cell-edit | (key, row, value) | Cell value edited |
column-resize | (key, width) | Column resized |