DataTable

An advanced React table that supports filtering, sorting, and pagination out of the box.
1<DataTableDemo />

Anatomy

Import and assemble the component:

1import {
2 DataTable,
3 DataTableColumnDef,
4 DataTableQuery,
5 DataTableSort,
6 useDataTable,
7 EmptyFilterValue
8} from "@raystack/apsara";
9
10<DataTable>
11 <DataTable.Toolbar>
12 <DataTable.Search />
13 <DataTable.Filters />
14 <DataTable.DisplayControls />
15 </DataTable.Toolbar>
16 <DataTable.Content />
17</DataTable>

API Reference

Root

Groups all parts of the data table and manages state.

Prop

Type

DisplayControls

Renders column visibility and display options.

Prop

Type

Filters

Renders the filter controls for table columns.

Prop

Type

Query

Describes the query state for server-side integration.

Prop

Type

ColumnDef

Describes the column configuration interface.

Prop

Type

Content

Renders the table content area.

Prop

Type

VirtualizedContent

For large datasets, use DataTable.VirtualizedContent for better performance. Parent container must have a fixed height.

Prop

Type

Examples

Virtualized Content

For large datasets, use DataTable.VirtualizedContent for better performance. Parent container must have a fixed height.

1<div style={{ height: 400 }}>
2 <DataTable
3 data={data}
4 columns={columns}
5 defaultSort={{ name: "name", order: "asc" }}>
6 <DataTable.Toolbar />
7 <DataTable.VirtualizedContent rowHeight={44} />
8 </DataTable>
9</div>

Fixed Width Columns

By default, columns share equal width. For fixed width columns, use flex: 'none' with width:

1const columns = [
2 {
3 accessorKey: "select",
4 header: "",
5 styles: {
6 header: { width: 50, flex: "none" },
7 cell: { width: 50, flex: "none" }
8 }
9 },
10 {
11 accessorKey: "name",
12 header: "Name"
13 // Uses flex: 1 1 0 by default (equal width)
14 }
15];

Basic Usage

1import { DataTable } from "@raystack/apsara";
2
3const columns = [
4 {
5 accessorKey: "name",
6 header: "Name",
7 columnType: "text",
8 enableSorting: true,
9 },
10 {
11 accessorKey: "age",
12 header: "Age",
13 columnType: "number",
14 enableSorting: true,
15 },
16];
17const data = [
18 { name: "John Doe", age: 30 },
19 { name: "Jane Smith", age: 25 },
20];
21function MyTable() {
22 return (
23 <DataTable
24 columns={columns}
25 data={data}
26 defaultSort={{ key: "name", order: "asc" }}>
27 <DataTable.Toolbar />
28 <DataTable.Content />
29 </DataTable>
30 );
31}

Column Configuration

Columns can be configured with various options:

Performance: Pass a stable columns reference (e.g. define at module scope or wrap in useMemo) so the table doesn't recompute filters, grouping, or sort state on every render.

Row ids: For sortable or filterable tables, pass getRowId so each row has a stable React key (e.g. getRowId={(row) => row.id} or getRowId={(row) => row.uuid}).

1interface DataTableColumnDef<TData, TValue> {
2 accessorKey: string; // Key to access data
3 header: string; // Column header text
4 columnType: "text" | "number" | "date" | "select"; // Data type
5 enableSorting?: boolean; // Enable sorting
6 enableColumnFilter?: boolean; // Enable filtering
7 enableHiding?: boolean; // Enable column visibility toggle
8 enableGrouping?: boolean; // Enable grouping
9 filterOptions?: FilterSelectOption[]; // Options for select filter
10 defaultHidden?: boolean; // Hide column by default
11}

Filtering

The DataTable supports multiple filter types:

Filter types:

  • Text: equals, not equals, contains,
  • Number: equals, not equals, less than, less than or equal, greater than, greater than or equal
  • Date: equals, not equals, before, on or before, after, on or after
  • Select: equals, not equals

Sorting

Enable column sorting:

1const columns = [
2 {
3 accessorKey: "name",
4 header: "Name",
5 enableSorting: true,
6 },
7];

Grouping

Group rows by same column data:

1const columns = [
2 {
3 accessorKey: "category",
4 header: "Category",
5 enableGrouping: true,
6 showGroupCount: true,
7 },
8];

Anchor group title

When grouping is enabled, you can make the current group label stick under the table header while scrolling (anchor group title) in both DataTable.Content and DataTable.VirtualizedContent by setting stickyGroupHeader={true} on the root. It is off by default.

1<DataTable
2 data={data}
3 columns={columns}
4 defaultSort={{ name: "name", order: "asc" }}
5 stickyGroupHeader={true}
6>
7 <DataTable.Toolbar />
8 <DataTable.Content />
9</DataTable>

Server-side Integration

1function ServerTable() {
2 const [data, setData] = useState([]);
3 const [totalRowCount, setTotalRowCount] = useState<number | undefined>();
4 const [query, setQuery] = useState();
5 const [isLoading, setIsLoading] = useState(false);
6 const handleQueryChange = async (query: DataTableQuery) => {
7 setIsLoading(true);
8 setQuery(query);
9 const response = await fetchData(query);
10 setData(response.data);
11 setTotalRowCount(response.totalRowCount);
12 setIsLoading(false);
13 };
14 return (
15 <DataTable
16 data={data}
17 totalRowCount={totalRowCount}
18 query={query}
19 columns={columns}
20 isLoading={isLoading}
21 mode="server"
22 onTableQueryChange={handleQueryChange}>
23 <DataTable.Toolbar />
24 <DataTable.Content />
25 </DataTable>
26 );
27}
  • In mode="client", footer count is computed from in-memory rows and shown as: X items hidden by filters
  • In mode="server":
    • if totalRowCount is provided, footer shows the same numeric pattern
    • if totalRowCount is missing, footer shows: Some items might be hidden by filters
  • Clear Filters clears both active filters and search.

Custom Styling

1const columns = [
2 {
3 accessorKey: "name",
4 header: "Name",
5 classNames: {
6 cell: "custom-cell",
7 header: "custom-header",
8 },
9 styles: {
10 cell: { fontWeight: "bold" },
11 header: { backgroundColor: "#f5f5f5" },
12 },
13 },
14];

Custom Cell Rendering

1const columns = [
2 {
3 accessorKey: "status",
4 header: "Status",
5 cell: ({ row }) => (
6 <Badge status={row.original.status}>{row.original.status}</Badge>
7 ),
8 },
9];

Using DataTable Filter

The DataTable.Filters component can be used separately to filter data for custom views.

1<DataTable
2data={data}
3query={query}
4columns={columns}
5mode="server"
6onTableQueryChange={handleQueryChange}>
7 <DataTable.Filters />
8</DataTable>

Using DataTable Display Controls

The DataTable.DisplayControls component can be used separately to display the display controls for custom views.

1<DataTable
2data={data}
3query={query}
4columns={columns}
5mode="server"
6onTableQueryChange={handleQueryChange}
7onColumnVisibilityChange={handleColumnVisibilityChange}>
8 <DataTable.DisplayControls />
9</DataTable>

The DataTable.Search component provides search functionality that automatically integrates with the table query. By default, it is disabled in zero state (when no data and no filters/search applied).

1<DataTable
2 data={data}
3 columns={columns}
4 defaultSort={{ name: "name", order: "asc" }}>
5 <DataTable.Search />
6 <DataTable.Content />
7</DataTable>

Search Auto-Disable Behavior

By default, DataTable.Search is automatically disabled in zero state to provide a better user experience. You can override this behavior:

1// Default: disabled in zero state
2<DataTable.Search />
3
4// Override: always enabled
5<DataTable.Search autoDisableInZeroState={false} />
6
7// Manual control: explicitly disable
8<DataTable.Search disabled={true} />

The search will be automatically enabled when:

  • Data exists in the table
  • Filters are applied
  • A search query is already present

Empty States

The DataTable supports two types of empty states to provide better user experience:

Zero State

Zero state is shown when no data has been fetched initially (no filters or search applied). In this state, the filter bar is automatically hidden.

1import { DataTable, EmptyState } from "@raystack/apsara";
2import { OrganizationIcon } from "@raystack/apsara/icons";
3
4<DataTable
5 data={[]}
6 columns={columns}
7 defaultSort={{ name: "name", order: "asc" }}>
8 <DataTable.Toolbar />
9 <DataTable.Content
10 zeroState={
11 <EmptyState
12 icon={<OrganizationIcon />}
13 heading="No users yet"
14 subHeading="Get started by creating your first user."
15 />
16 }
17 />
18</DataTable>

Empty State

Empty state is shown when initial data exists but no results match after applying filters or search. In this state, the filter bar remains visible so users can adjust their filters.

1import { DataTable, EmptyState } from "@raystack/apsara";
2import { OrganizationIcon, FilterIcon } from "@raystack/apsara/icons";
3
4<DataTable
5 data={initialData}
6 columns={columns}
7 defaultSort={{ name: "name", order: "asc" }}>
8 <DataTable.Toolbar />
9 <DataTable.Search />
10 <DataTable.Content
11 zeroState={
12 <EmptyState
13 icon={<OrganizationIcon />}
14 heading="No users yet"
15 subHeading="Get started by creating your first user."
16 />
17 }
18 emptyState={
19 <EmptyState
20 icon={<FilterIcon />}
21 heading="No users found"
22 subHeading="We couldn't find any matches for that keyword or filter. Try alternative terms or check for typos."
23 />
24 }
25 />
26</DataTable>

Fallback Behavior

  • If zeroState is not provided, it falls back to emptyState
  • If neither zeroState nor emptyState is provided, a default empty state is shown
  • The filter bar visibility is automatically controlled based on the state

Custom Empty State Content

You can provide custom React components for both states:

1import { DataTable, EmptyState, Flex, Text, Button } from "@raystack/apsara";
2import { OrganizationIcon, FilterIcon } from "@raystack/apsara/icons";
3
4<DataTable.Content
5 zeroState={
6 <Flex direction="column" gap={4} align="center" style={{ padding: "40px" }}>
7 <OrganizationIcon width={48} height={48} style={{ opacity: 0.5 }} />
8 <Flex direction="column" gap={2} align="center">
9 <Text size={4} weight="medium">
10 No data available
11 </Text>
12 <Text size={2} style={{ color: "var(--rs-color-text-subtle)" }}>
13 There are no users in the system. Create your first user to get started.
14 </Text>
15 </Flex>
16 <Button size="small">Create User</Button>
17 </Flex>
18 }
19 emptyState={
20 <EmptyState
21 icon={<FilterIcon />}
22 heading="No matches found"
23 subHeading="Try adjusting your filters or search query."
24 />
25 }
26/>

Row selection

DataTable does not ship a built-in selection toolbar, but the underlying TanStack table instance is exposed via useDataTable(), so you can wire row selection yourself and float an FloatingActions bar over the table when rows are selected.

1<DataTableSelectionDemo />

Add a leading checkbox column to enable selection, then render a FloatingActions overlay driven by table.getSelectedRowModel():

1import {
2 Button,
3 Checkbox,
4 Chip,
5 DataTable,
6 FloatingActions,
7 useDataTable,
8} from "@raystack/apsara";
9import { TransformIcon } from "@radix-ui/react-icons";
10
11// 1. Leading checkbox column that wires TanStack selection.
12const selectionColumn = {
13 id: "select",
14 header: ({ table }) => (
15 <Checkbox
16 checked={
17 table.getIsAllRowsSelected()
18 ? true
19 : table.getIsSomeRowsSelected()
20 ? "indeterminate"
21 : false
22 }
23 onCheckedChange={(v) => table.toggleAllRowsSelected(Boolean(v))}
24 />
25 ),
26 cell: ({ row }) => (
27 <Checkbox
28 checked={row.getIsSelected()}
29 onCheckedChange={(v) => row.toggleSelected(Boolean(v))}
30 onClick={(e) => e.stopPropagation()}
31 />
32 ),
33 enableSorting: false,
34 enableColumnFilter: false,
35 enableHiding: false,
36};
37
38// 2. Render the overlay bar alongside DataTable, reading selection from context.
39function SelectionBar() {
40 const { table } = useDataTable();
41 const selected = table.getSelectedRowModel().rows;
42 if (selected.length === 0) return null;
43
44 return (
45 <div
46 style={{
47 position: "absolute",
48 bottom: "var(--rs-space-9)",
49 left: "50%",
50 transform: "translateX(-50%)",
51 zIndex: 2,
52 }}
53 >
54 <FloatingActions aria-label="Selection actions">
55 <Chip
56 variant="outline"
57 size="large"
58 color="neutral"
59 leadingIcon={<TransformIcon />}
60 isDismissible
61 onDismiss={() => table.resetRowSelection()}
62 >
63 {selected.length} selected
64 </Chip>
65 <FloatingActions.Separator />
66 <Button variant="outline" color="neutral" size="small">
67 Move to
68 </Button>
69 <Button variant="outline" color="neutral" size="small">
70 Actions
71 </Button>
72 </FloatingActions>
73 </div>
74 );
75}
76
77// 3. Compose. The wrapping element must be `position: relative` so the
78// overlay anchors to the table region (not the viewport).
79<div style={{ position: "relative", flex: 1, minHeight: 0 }}>
80 <DataTable
81 data={rows}
82 columns={[selectionColumn, ...columns]}
83 mode="client"
84 defaultSort={{ name: "name", order: "asc" }}
85 >
86 <DataTable.Toolbar />
87 <DataTable.Search />
88 <DataTable.Content />
89 <SelectionBar />
90 </DataTable>
91</div>

Notes:

  • table.resetRowSelection() clears the selection; wire it to Chip's onDismiss.
  • The overlay uses position: absolute on the parent wrapper, so the last row of data isn't hidden behind it — if your table is tall, add padding-bottom on DataTable.Content's scroll container to match.
  • When DataTable ships first-class row selection, this pattern will migrate to a selection-aware helper; the user-level API (the chip + buttons inside FloatingActions) stays the same.

Accessibility

  • Uses semantic table, thead, tbody, tr, th, and td elements
  • Supports keyboard navigation between cells and rows
  • Sort state is communicated via aria-sort attribute
  • Filter controls are accessible via keyboard