Separate the table logic from the rendering. The core is a headless hook, in the style of TanStack Table. Headless means it handles logic and state but renders no markup.
The hook takes columns and data. It returns row models plus state. The design system ships a default rendered DataTable on top of the hook. Teams with unusual layouts can use the hook directly.
Each piece of state is uncontrolled by default, with an optional controlled pair. The pieces are:
- sorting
- filters
- pagination
- row selection
- column visibility
For example, sorting has the controlled pair sorting and onSortingChange. This matters because teams differ. Some teams sort locally. Others need server-side sorting, so they must lift the state into the URL and the query key. A manualSorting or manualPagination flag tells the table not to process the data itself.
Columns are declared as config with:
accessorheadercellrender functionsenableSorting
For large datasets, I support virtualization, which renders only the visible rows. I also support a stable getRowId, so selection stays correct across pages.
For accessibility:
- Use real
tablesemantics. - Put
aria-sorton the headers. - Make every control reachable by keyboard.
- Headless logic is separated from rendering.
- Each piece of state can optionally be controlled by the parent.
- A manual mode supports server-side sorting and pagination.
- Stable row ids keep selection correct, and virtualization handles large data.
- Use real table semantics and
aria-sorton headers.
- TanStack (https://tanstack.com/)tanstack.com