Volt UI
ComponentsData Table

Selection & Actions

Row selection, bulk actions, row actions, and clickable rows.

Row Selection

Enable row selection with checkboxes using the enableRowSelection prop.

InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.garcia@example.com
€476.00
0 of 10 row(s) selected
import { useState } from "react"
import { DataTable, DataTableContent, DataTableFooter } from "@epilot/volt-ui"

function MyTable() {
  const [rowSelection, setRowSelection] = useState({})

  return (
    <DataTable
      columns={columns}
      data={data}
      enableRowSelection
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
    >
      <DataTableContent />
      <DataTableFooter>
        <span className="text-sm text-gray-light">
          {Object.keys(rowSelection).length} of {data.length} row(s) selected
        </span>
      </DataTableFooter>
    </DataTable>
  )
}

Accessing Selected Row Data

The rowSelection state contains row IDs, not the actual data. To get the selected row data, use the useDataTable hook:

0 of 10 selected
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.garcia@example.com
€476.00
import { useDataTable, Button } from "@epilot/volt-ui"

function BulkActions() {
  const { table } = useDataTable()

  // Get selected rows as Row<TData>[]
  const selectedRows = table.getSelectedRowModel().rows

  // Get the actual data from selected rows
  const selectedData = selectedRows.map(row => row.original)

  const handleBulkDelete = () => {
    // selectedData contains the full row objects
    console.log('Deleting:', selectedData)
    deleteItems(selectedData.map(item => item.id))
  }

  if (selectedRows.length === 0) return null

  return (
    <div className="flex items-center gap-2">
      <span>{selectedRows.length} selected</span>
      <Button variant="secondary" size="sm" onClick={handleBulkDelete} destructive>
        Delete selected
      </Button>
    </div>
  )
}

function MyTable() {
  const [rowSelection, setRowSelection] = useState({})

  return (
    <DataTable
      columns={columns}
      data={data}
      enableRowSelection
      rowSelection={rowSelection}
      onRowSelectionChange={setRowSelection}
    >
      <DataTableToolbar>
        <BulkActions />
      </DataTableToolbar>
      <DataTableContent />
    </DataTable>
  )
}

Key methods:

  • table.getSelectedRowModel().rows - Array of selected Row<TData> objects
  • row.original - The original data object for that row
  • table.getSelectedRowModel().flatRows - Flattened array (useful with grouped rows)

From Parent Component

Use the getSelectedRowData helper to derive selected data from the controlled rowSelection state. This works with multi-select.

0 of 10 selected
InvoiceStatusTypeCustomerEmail
Amount
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.garcia@example.com
€476.00
import { useState, useMemo } from "react"
import { DataTable, DataTableContent, Button, getSelectedRowData } from "@epilot/volt-ui"

function MyPage() {
  const [rowSelection, setRowSelection] = useState({})

  // Use helper to get selected data
  const selectedData = useMemo(
    () => getSelectedRowData(rowSelection, data),
    [rowSelection, data]
  )

  const handleBulkDelete = () => {
    console.log('Deleting:', selectedData)
    // Call your API, update state, etc.
  }

  return (
    <div>
      {/* Actions outside the table */}
      <div className="mb-4 flex items-center gap-2">
        <span>{selectedData.length} selected</span>
        <Button
          variant="secondary"
          size="sm"
          onClick={handleBulkDelete}
          disabled={selectedData.length === 0}
          destructive
        >
          Delete selected
        </Button>
      </div>

      <DataTable
        columns={columns}
        data={data}
        enableRowSelection
        rowSelection={rowSelection}
        onRowSelectionChange={setRowSelection}
        getRowId={(row) => row.id}
      >
        <DataTableContent />
      </DataTable>
    </div>
  )
}

Full Control

If you need more control over the selection logic, you can implement it manually:

const selectedData = useMemo(() => {
  const selectedIds = Object.keys(rowSelection).filter(id => rowSelection[id])
  return data.filter(item => selectedIds.includes(item.id))
}, [rowSelection, data])

Important: When using controlled selection with custom row IDs, ensure getRowId on the DataTable returns the same ID format you use to look up items in your data array.

Row Actions

Add a dropdown menu for row-level actions using an actions column.

InvoiceStatusCustomerEmail
Amount
Actions
INV-0001PaidJohn Doejohn.doe@example.com
€321.00
INV-0002PendingJane Smithjane.smith@example.com
€106.00
INV-0003PendingBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverdueAlice Brownalice.brown@example.com
€421.00
INV-0005OverdueCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingGrace Leegrace.lee@example.com
€299.00
INV-0009PaidHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidIsabel Garciaisabel.garcia@example.com
€476.00
import { DataTable, DataTableContent, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Button } from "@epilot/volt-ui"
import { IconDotsVertical, IconEye, IconPencil, IconCopy, IconTrash } from "@tabler/icons-react"
import type { ColumnDef } from "@tanstack/react-table"

const columnsWithActions: ColumnDef<Invoice>[] = [
  { accessorKey: "id", header: "Invoice" },
  { accessorKey: "status", header: "Status" },
  { accessorKey: "customer", header: "Customer" },
  {
    id: "actions",
    header: () => <span className="sr-only">Actions</span>,
    cell: ({ row }) => {
      const invoice = row.original
      return (
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="tertiary" size="icon-sm">
              <IconDotsVertical size={16} />
              <span className="sr-only">Open menu</span>
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuItem>
              <IconEye size={16} />
              View details
            </DropdownMenuItem>
            <DropdownMenuItem>
              <IconPencil size={16} />
              Edit
            </DropdownMenuItem>
            <DropdownMenuItem onClick={() => navigator.clipboard.writeText(invoice.id)}>
              <IconCopy size={16} />
              Copy ID
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem destructive>
              <IconTrash size={16} />
              Delete
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      )
    },
    size: 48,
  },
]

<DataTable columns={columnsWithActions} data={data} enableSorting>
  <DataTableContent />
</DataTable>

Clickable Rows

Make entire rows clickable using the onRowClick prop. Rows show a blue hover state and trigger an action when clicked. Interactive elements like action menus and checkboxes still work independently.

InvoiceStatusTypeCustomerEmail
Amount
Actions
INV-0001PaidcreditJohn Doejohn.doe@example.com
€321.00
INV-0002PendingdebitJane Smithjane.smith@example.com
€106.00
INV-0003PendingdebitBob Wilsonbob.wilson@example.com
€348.00
INV-0004OverduecreditAlice Brownalice.brown@example.com
€421.00
INV-0005OverduecreditCharlie Davischarlie.davis@example.com
€399.00
INV-0006PendingcreditEva Martinezeva.martinez@example.com
€529.00
INV-0007PendingdebitFrank Millerfrank.miller@example.com
€546.00
INV-0008PendingcreditGrace Leegrace.lee@example.com
€299.00
INV-0009PaidcreditHenry Chenhenry.chen@example.com
€387.00
INV-0010PaidcreditIsabel Garciaisabel.garcia@example.com
€476.00
import { DataTable, DataTableContent } from "@epilot/volt-ui"

function MyTable() {
  const handleRowClick = (row) => {
    // Navigate to detail page or open modal
    router.push(`/invoice/${row.original.id}`)
  }

  return (
    <DataTable
      columns={columns}
      data={data}
      onRowClick={handleRowClick}
    >
      <DataTableContent />
    </DataTable>
  )
}

When using onRowClick with action menus or checkboxes, clicks on those interactive elements won't trigger the row click handler - they work independently.

getSelectedRowData Helper

Utility function to extract selected row data from selection state.

import { getSelectedRowData } from "@epilot/volt-ui"

const selectedData = useMemo(
  () => getSelectedRowData(rowSelection, data),
  [rowSelection, data]
)
ParameterTypeDefaultDescription
rowSelectionRowSelectionState-The selection state from DataTable
dataTData[]-Your data array
getRowId(row: TData) => stringrow.idOptional ID accessor

Returns: TData[] - Array of selected data items