Appica UIUI 1.1.0

Data Table

A data table recipe - Appica UI components wired to TanStack Table for sorting, filtering, selection, column visibility, and pagination.

A data table isn't a single component. It's a recipe: you keep your own column model and feature logic in TanStack Table (a headless, unstyled engine) and render its output with Appica UI components: Table, Checkbox, Thumbnail, Badge, Input, Dropdown Menu, and Scroll Area.

This page is one complete, copy-pasteable example rather than a packaged component - adapt the columns and data to your own shape.

Installation

TanStack Table is the only extra dependency. It ships its own types, so nothing else is needed.

pnpm add @tanstack/react-table

Example

A transactions table with row selection, a searchable global filter, sortable columns, a show/hide columns menu, per-row actions, and client pagination. Resize the frame down and the table scrolls horizontally instead of cramping.

How it works

TanStack Table owns the state and the row model; Appica UI owns the rendering. The bridge is flexRender, which turns each column's header/cell definition into React nodes that you drop straight into Table parts:

TanStack Table v9 is opt-in: nothing but the core row model ships by default, so every feature the table uses is registered once in a tableFeatures() object declared outside the component, together with the row models and filter/sort functions those features need. Pass it to useTable as features:

A row-model slot has to follow its feature (sortedRowModel needs rowSortingFeature), and a name you reference as a string has to be registered: globalFilterFn: 'includesString' resolves against the filterFns slot, and the default 'auto' column sorting picks alphanumeric or text out of sortFns. Each feature then pairs with a piece of React state and its on…Change callback:

  • Row selection - rowSelectionFeature plus a select column whose header and cell render a Checkbox. The header checkbox wires checked to getIsAllPageRowsSelected() and indeterminate to getIsSomePageRowsSelected() && !getIsAllPageRowsSelected() for the mixed-state dash - getIsSomePageRowsSelected() means "at least one" and stays true once every row is selected, so the all-selected case has to be excluded explicitly. Mark the row highlighted when row.getIsSelected() to tint it.
  • Sorting - rowSortingFeature and createSortedRowModel() plus a SortableHeader button that calls column.toggleSorting() and swaps a ChevronsUpDown / ChevronUp / ChevronDown icon off column.getIsSorted().
  • Global filter - globalFilteringFeature and createFilteredRowModel() with globalFilterFn: 'includesString', driven by an Input whose startSlot holds a search icon.
  • Column visibility - columnVisibilityFeature and a Dropdown Menu of DropdownMenuCheckboxItems, one per table.getAllColumns().filter((c) => c.getCanHide()), each toggling column.toggleVisibility().
  • Pagination - rowPaginationFeature and createPaginatedRowModel() with an initialState.pagination, with Previous / Next Buttons gated on getCanPreviousPage() / getCanNextPage(). Read the current page off table.state.pagination.
  • Per-row actions - a Dropdown Menu in a trailing actions column. Mark it enableSorting: false and enableHiding: false so it stays put.

Rich cells

Because a cell returns arbitrary JSX, you compose the same components you'd use anywhere. The description cell pairs a Thumbnail (an icon-soft variant for category icons, an image for brand logos) with an emphasized label, and the category cell renders an outline Badge with a small color dot - keeping the badge neutral while the dot carries the category color:

Horizontal scroll

Wrap the table in a horizontal Scroll Area and keep cells from wrapping with whitespace-nowrap, so a wide table scrolls sideways on small screens instead of squeezing its columns:

Accessibility

  • The markup is a real <table> - flexRender only fills the cells, so screen readers still announce rows, columns, and headers natively. See the Table notes.
  • Give every selection Checkbox an aria-label ("Select all rows", "Select row") - the checkbox has no visible text of its own.
  • Sort controls are real <button>s inside the <th>, so they're keyboard-focusable and operable with Enter/Space.
  • The per-row actions trigger needs an aria-label (e.g. "Open actions menu") since it's an icon-only button.
  • Row highlighted is a background cue only; selection state is conveyed by the checkbox, not by color alone.

© 2026 Appica UI. A free component library, crafted by the Appica team.