Proper UI

Data table

A React data table with sorting, search, faceted and range filters, column visibility, pinning and resizing, row selection with bulk actions, pagination, server-side mode and virtualization. Built on React Aria and styled with Tailwind CSS.

Directory

Search, filter, sort by several columns and choose which columns to show.

Name
Status
Role
Team
Start date
Salary
Location

Olivia Rhye

olivia.rhye@proper.example

ActiveProduct DesignerDesignJan 1, 2019$60,000Lisbon

Phoenix Diggs

phoenix.diggs@proper.example

ActiveBackend DeveloperSupportJun 12, 2020$149,000Toronto

Lana Mays

lana.mays@proper.example

SuspendedData AnalystSalesNov 23, 2021$148,000Singapore

Demi Craig

demi.craig@proper.example

ActiveProduct ManagerMarketingApr 6, 2022$147,000Melbourne

Candice Hall

candice.hall@proper.example

InvitedQA EngineerEngineeringSep 17, 2023$146,000Berlin

Natali Wilkinson

natali.wilkinson@proper.example

ActiveSupport LeadProductFeb 28, 2024$145,000Nairobi

Drew Wright

drew.wright@proper.example

ActiveFrontend DeveloperDesignJul 11, 2025$144,000Austin

Orlando Baker

orlando.baker@proper.example

SuspendedUX ResearcherSupportDec 22, 2019$143,000Lisbon

Andi Lane

andi.lane@proper.example

ActiveProduct DesignerSalesMay 5, 2020$142,000Toronto

Kate Hewitt

kate.hewitt@proper.example

InvitedBackend DeveloperMarketingOct 16, 2021$141,000Singapore

1–10 of 48

Installation

npx properui@latest add data-table

Usage

DataTable takes your records and a list of column definitions, and does the rest: sorting, search, filters, column visibility, selection, pagination, density, and the loading and empty states.

import { DataTable } from "@properui/ui/components/application/data-table/data-table";
import type { DataTableColumn } from "@properui/ui/components/application/data-table/use-data-table";

const columns: DataTableColumn<Person>[] = [
    { id: "name", header: "Name", isRowHeader: true, enableSorting: true },
    { id: "status", header: "Status", enableSorting: true, filter: { type: "select" } },
    { id: "salary", header: "Salary", enableSorting: true, filter: { type: "number-range" } },
    { id: "startDate", header: "Start date", filter: { type: "date-range" } },
];

<DataTable title="People" data={people} columns={columns} selectionMode="multiple" pageSizeOptions={[10, 25, 50]} />;

A column reads row[id] unless you give it an accessor, and renders that value as text unless you give it a cell. Give exactly one column isRowHeader: it names each row for assistive technology.

Table stays the styling primitive. DataTable renders the same TableCard, Table.Header, Table.Row and TablePaginationNumbered the table components use, so every visual detail is shared and Table's own API is unchanged. Reach for Table directly when you want a static or hand-wired table; reach for DataTable when you want the behaviour.

Sorting

Set enableSorting on a column and click its header to sort; click again to reverse. With enableMultiSort, earlier sorts are kept as tie-breakers: the column you click last becomes the primary sort and a small number in each sorted header shows its priority. Pass a sortingFn for a custom comparator.

Team members

8 people
Name
Status
Role
Start date
Salary

Candice Hall

candice.hall@proper.example

InvitedQA EngineerSep 17, 2023$146,000

Demi Craig

demi.craig@proper.example

ActiveProduct ManagerApr 6, 2022$147,000

Drew Wright

drew.wright@proper.example

ActiveFrontend DeveloperJul 11, 2025$144,000

Lana Mays

lana.mays@proper.example

SuspendedData AnalystNov 23, 2021$148,000

Natali Wilkinson

natali.wilkinson@proper.example

ActiveSupport LeadFeb 28, 2024$145,000

Olivia Rhye

olivia.rhye@proper.example

ActiveProduct DesignerJan 1, 2019$60,000

Orlando Baker

orlando.baker@proper.example

SuspendedUX ResearcherDec 22, 2019$143,000

Phoenix Diggs

phoenix.diggs@proper.example

ActiveBackend DeveloperJun 12, 2020$149,000

Filters and column visibility

The toolbar holds the search field, one faceted button per select filter (its options and counts come from the data unless you pass options), and a Filters panel for text, number-range and date-range filters; the date range uses DateRangePicker. The Columns menu shows and hides columns, and showColumnMenu adds a sort, pin and hide menu to every header.

Selection and bulk actions

selectionMode="multiple" adds a checkbox column. Once a row is selected, a bar shows the count, your bulkActions, and a "Clear selection" button. onSelectionChange reports both the React Aria selection and the selected records.

Members

24 people
Name
Status
Role
Team
Start date
Salary

Olivia Rhye

olivia.rhye@proper.example

ActiveProduct DesignerDesignJan 1, 2019$60,000

Phoenix Diggs

phoenix.diggs@proper.example

ActiveBackend DeveloperSupportJun 12, 2020$149,000

Lana Mays

lana.mays@proper.example

SuspendedData AnalystSalesNov 23, 2021$148,000

Demi Craig

demi.craig@proper.example

ActiveProduct ManagerMarketingApr 6, 2022$147,000

Candice Hall

candice.hall@proper.example

InvitedQA EngineerEngineeringSep 17, 2023$146,000

Natali Wilkinson

natali.wilkinson@proper.example

ActiveSupport LeadProductFeb 28, 2024$145,000

Drew Wright

drew.wright@proper.example

ActiveFrontend DeveloperDesignJul 11, 2025$144,000

Orlando Baker

orlando.baker@proper.example

SuspendedUX ResearcherSupportDec 22, 2019$143,000

Andi Lane

andi.lane@proper.example

ActiveProduct DesignerSalesMay 5, 2020$142,000

Kate Hewitt

kate.hewitt@proper.example

InvitedBackend DeveloperMarketingOct 16, 2021$141,000

1–10 of 24

Virtualization

virtualized renders the table through React Aria's Virtualizer and TableLayout, so only the rows in view exist in the DOM. Rows have a fixed height per density, the header stays in view, and pagination is off by default. Column pinning is not applied in this mode.

All employees

5,000 rows
Name

Server-side mode

Set manual and the table stops sorting, filtering and paging locally. It renders data as the current page, uses rowCount for the page count, and calls onQueryChange on mount and after every change with everything a request needs:

const onQueryChange = ({ sorting, globalFilter, columnFilters, pagination }: DataTableQuery) => {
    setIsLoading(true);
    fetchPeople({ sorting, globalFilter, columnFilters, pagination }).then(({ rows, total }) => {
        setResult({ rows, total });
        setIsLoading(false);
    });
};

<DataTable manual data={result.rows} rowCount={result.total} isLoading={isLoading} onQueryChange={onQueryChange} columns={columns} />;

isLoading swaps the rows for skeleton rows. Searching or filtering resets the query to page 1. Debounce or cancel stale requests yourself: the demo below drops any response that is not for the latest query.

Employees

0 results

Sorted, filtered and paginated on the server.

Name
Status
Role
Salary
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

0–0 of 0

Pinned columns and sticky header

pinned: "start" or pinned: "end" keeps a column in view while the table scrolls sideways. Give pinned columns a width, which positions the columns pinned after them. stickyHeader keeps the header row visible while the body scrolls inside maxHeight.

Compensation

Name
Status
Role
Team
Location
Email
Start date
Salary

Olivia Rhye

ActiveProduct DesignerDesignLisbonolivia.rhye@proper.exampleJan 1, 2019$60,000

Phoenix Diggs

ActiveBackend DeveloperSupportTorontophoenix.diggs@proper.exampleJun 12, 2020$149,000

Lana Mays

SuspendedData AnalystSalesSingaporelana.mays@proper.exampleNov 23, 2021$148,000

Demi Craig

ActiveProduct ManagerMarketingMelbournedemi.craig@proper.exampleApr 6, 2022$147,000

Candice Hall

InvitedQA EngineerEngineeringBerlincandice.hall@proper.exampleSep 17, 2023$146,000

Natali Wilkinson

ActiveSupport LeadProductNairobinatali.wilkinson@proper.exampleFeb 28, 2024$145,000

Drew Wright

ActiveFrontend DeveloperDesignAustindrew.wright@proper.exampleJul 11, 2025$144,000

Orlando Baker

SuspendedUX ResearcherSupportLisbonorlando.baker@proper.exampleDec 22, 2019$143,000

Andi Lane

ActiveProduct DesignerSalesTorontoandi.lane@proper.exampleMay 5, 2020$142,000

Kate Hewitt

InvitedBackend DeveloperMarketingSingaporekate.hewitt@proper.exampleOct 16, 2021$141,000

Ava Cano

ActiveData AnalystEngineeringMelbourneava.cano@proper.exampleMar 27, 2022$140,000

Koray Franz

ActiveProduct ManagerProductBerlinkoray.franz@proper.exampleAug 10, 2023$139,000

Mollie Wu

SuspendedQA EngineerDesignNairobimollie.wu@proper.exampleJan 21, 2024$138,000

Eduard Okumus

ActiveSupport LeadSupportAustineduard.okumus@proper.exampleJun 4, 2025$137,000

Zahir Steiner

InvitedFrontend DeveloperSalesLisbonzahir.steiner@proper.exampleNov 15, 2019$136,000

Sienna Morrison

ActiveUX ResearcherMarketingTorontosienna.morrison@proper.exampleApr 26, 2020$135,000

Olivia Baker

ActiveProduct DesignerEngineeringSingaporeolivia.baker16@proper.exampleSep 9, 2021$134,000

Phoenix Lane

SuspendedBackend DeveloperProductMelbournephoenix.lane17@proper.exampleFeb 20, 2022$133,000

Lana Hewitt

ActiveData AnalystDesignBerlinlana.hewitt18@proper.exampleJul 3, 2023$132,000

Demi Cano

InvitedProduct ManagerSupportNairobidemi.cano19@proper.exampleDec 14, 2024$131,000

Resizable columns

resizable wraps the table in React Aria's ResizableTableContainer and adds a ColumnResizer to every header. Drag the handle, or focus it and use the arrow keys. width, minWidth and maxWidth on a column set its starting width and its limits.

Resizable columns

Name
Status
Role
Start date
Salary

Olivia Rhye

olivia.rhye@proper.example

ActiveProduct DesignerJan 1, 2019$60,000

Phoenix Diggs

phoenix.diggs@proper.example

ActiveBackend DeveloperJun 12, 2020$149,000

Lana Mays

lana.mays@proper.example

SuspendedData AnalystNov 23, 2021$148,000

Demi Craig

demi.craig@proper.example

ActiveProduct ManagerApr 6, 2022$147,000

Candice Hall

candice.hall@proper.example

InvitedQA EngineerSep 17, 2023$146,000

Natali Wilkinson

natali.wilkinson@proper.example

ActiveSupport LeadFeb 28, 2024$145,000

Building your own layout

useDataTable is the hook behind DataTable. It returns the processed rows, the visibleColumns, sortDescriptor/onSortChange for Table, and setters for every piece of state. Pass its return value to DataTableToolbar, DataTableViewOptions, DataTableFacetedFilter or DataTableColumnMenu to reuse the built-in controls in a layout of your own.

FAQs