Use this skill when building a DataTable for bulk data analysis, comparison, sorting, or filtering — including column configuration, the useLocalQuery hook, row action menus, empty/loading states, and color mode. Do not use for item management with row expansion or selection (see gamut-datalist), or for small static lists (see gamut-list).
Installs into .claude/skills of the current project.
Are you the author of Gamut Datatable?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/codecademy-gamut-datatable)
---
name: gamut-datatable
description: Use this skill when building a DataTable for bulk data analysis, comparison, sorting, or filtering — including column configuration, the useLocalQuery hook, row action menus, empty/loading states, and color mode. Do not use for item management with row expansion or selection (see gamut-datalist), or for small static lists (see gamut-list).
---
# Gamut DataTable
Structured, query-capable table for bulk data analysis and comparison. Sorting, filtering, loading, and empty states are built in. Use when the goal is to scan and compare information across rows — not to manage individual items.
Source: `@codecademy/gamut` — `packages/gamut/src/DataList/DataTable.tsx`
See also: [`gamut-datalist`](../gamut-datalist/SKILL.md) — item-focused list with expansion and selection. [`gamut-list`](../gamut-list/SKILL.md) — lower-level list primitives for fully custom layouts. [`gamut-accessibility`](../gamut-accessibility/SKILL.md) — ARIA and keyboard interaction. [`gamut-color-mode`](../gamut-color-mode/SKILL.md) — dark/light mode with `Background`. [`gamut-z-index`](../gamut-z-index/SKILL.md) — why the row-menu-opens-Modal pattern below needs `inline` to work.
Storybook: [Organisms / Lists & Tables / DataTable](https://gamut.codecademy.com/?path=/docs-organisms-lists-tables-datatable--docs)
## Components
```tsx
import { DataTable } from '@codecademy/gamut';
import { useLocalQuery } from '@codecademy/gamut';
```
| Symbol | Role |
| --------------- | ----------------------------------------------------------------------------------------- |
| `DataTable` | Root component; always renders as a `table`-variant `List` with scrolling enabled |
| `useLocalQuery` | Client-side hook for sort/filter state; spread its return value directly into `DataTable` |
## When to use DataTable
- Displaying data that users need to **compare across rows** (metrics, scores, statuses, dates).
- When the data set needs **sorting, filtering, or both** and that logic lives client-side.
- Dashboards, admin tables, reports, and data-dense views.
- When rows are **not individually selectable or expandable** — use `DataList` if you need those.
**Do not use DataTable when:**
- Users engage with items individually (expand for details, select for bulk actions) → use [`DataList`](../gamut-datalist/SKILL.md).
- The list is small, static, and needs fully custom row layouts → use [`List`](../gamut-list/SKILL.md) directly.
- There is no data at all and you just need a layout container.
## Design principles
- **Prioritize comparison**: arrange columns to encourage scanning and finding patterns, not storytelling.
- **Surface secondary info on drill-down**: use Coachmarks, Tooltips, Modals, or Flyovers rather than cramming detail into table cells.
- **Avoid information overload**: determine what belongs en-masse vs. what should live on a detail surface. Order columns by priority; collapse lower-priority columns at smaller sizes.
- **Use cell-level interactions carefully**: anchors and links for navigation; popovers for in-context actions.
## Props
| Prop | Type | Default | Notes |
| ----------------------- | ------------------------- | ------------------- | ------------------------------------------------------------------- |
| `id` | `string` | required | Unique ID for the table |
| `idKey` | `keyof Row` | required | Row identifier — must be a `string \| number` field |
| `rows` | `Row[]` | required | Data array |
| `columns` | `ColumnConfig<Row>[]` | required | Column definitions |
| `query` | `Query<Row>` | — | Current sort/filter state; use `useLocalQuery` or manage externally |
| `onQueryChange` | `OnQueryChange<Row>` | — | Called when sort or filter changes |
| `loading` | `boolean` | `false` | Replaces row content with shimmer placeholders |
| `spacing` | `'condensed' \| 'normal'` | `'condensed'` | Row padding |
| `scrollable` | `boolean` | `true` | Enables horizontal scroll with sticky first column |
| `shadow` | `boolean` | `false` | Shows a right-side scroll-indicator shadow |
| `height` | `string` | `'100%'` | Container height when `scrollable` is true |
| `minHeight` | `number` | `0` | Minimum container height |
| `emptyMessage` | `ReactNode` | default empty state | Rendered when `rows` is empty |
| `showOverflow` | `boolean` | — | Shows overflow indicators in cells |
| `disableContainerQuery` | `boolean` | `false` | Falls back to media queries instead of container queries |
| `scrollToTopOnUpdate` | `boolean` | `false` | Scrolls to top when rows change |
| `wrapperWidth` | `string` | — | Custom wrapper width override |
## ColumnConfig
| Field | Type | Notes |
| ---------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `key` | `keyof Row` | Required; maps to a row data field |
| `header` | `string` | Column header label |
| `type` | `'content' \| 'header' \| 'control' \| 'select' \| 'expand' \| 'expandControl' \| 'orderedHeader' \| 'tableControl'` | `ListColProps['type']`; `'content'` is default — see [Column `type`](#column-type) |
| `size` | `'content' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'content'` (default) fits cell contents; use `sm`–`xl` for fixed column widths |
| `fill` | `boolean` | Expands the column to fill remaining width |
| `justify` | `'left' \| 'right'` | Cell alignment |
| `sortable` | `boolean` | Adds a sort toggle to the column header |
| `filters` | `string[]` | Adds a filter dropdown with these string values |
| `options` | `Array<string \| { text: string; value: string }>` | Alternative to `filters` when display text differs from value |
| `render` | `(row: Row) => ReactElement \| null` | Custom cell renderer |
### Column `type`
| `type` | Use for |
| --------------- | --------------------------------------------------------------------------- |
| `content` | Default; secondary data columns |
| `header` | Primary label column; sticky when the table is scrollable |
| `control` | Action controls (buttons, menus); right-aligned, reduced padding |
| `select` | Checkbox column — DataList adds this when `selectable` is set |
| `expand` | Expanded content area |
| `expandControl` | Expand toggle column — DataList adds this when `expandable` is set |
| `orderedHeader` | Internal; applied by `ListCol` for ordered lists — do not set manually |
| `tableControl` | Internal; applied by table rows for `type: 'control'` — do not set manually |
**DataTable** columns typically use `header` (sticky identifier) and `control` (row actions). Other values are inherited from `ListCol` for API parity; selection and expansion columns are handled by [`DataList`](../gamut-datalist/SKILL.md), not `ColumnConfig`.
## Basic usage
```tsx
import { DataTable, useLocalQuery } from '@codecademy/gamut';
const columns = [
{ key: 'name', header: 'Name', size: 'md', sortable: true },
{
key: 'role',
header: 'Role',
size: 'md',
filters: ['Engineer', 'Design', 'Product'],
},
{
key: 'score',
header: 'Score',
size: 'sm',
sortable: true,
justify: 'right',
},
];
const MyTable = ({ data }) => {
const queryData = useLocalQuery({ idKey: 'id', rows: data, columns });
return (
<DataTable id="my-table" idKey="id" columns={columns} {...queryData} />
);
};
```
## Custom cell render
Use `render` for cells that need non-text content (status badges, progress bars, action buttons).
```tsx
const columns = [
{ key: 'name', header: 'Name', size: 'md', type: 'header' },
{
key: 'status',
header: 'Status',
size: 'sm',
render: (row) => (
<Badge variant={row.status === 'active' ? 'green' : 'red'}>
{row.status}
</Badge>
),
},
];
```
## Row action menus
Use a `type: 'control'` column with `PopoverContainer` and `Menu` for row-level actions.
```tsx
import {
DataTable,
IconButton,
Menu,
MenuItem,
Modal,
PopoverContainer,
} from '@codecademy/gamut';
import { MiniKebabMenuIcon } from '@codecademy/gamut-icons';
import { useRef, useState } from 'react';
const RowActions = ({ rowId }) => {
const [isOpen, setIsOpen] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const ref = useRef(null);
return (
<Box display="inline-block" p={8} ref={ref}>
<IconButton
icon={MiniKebabMenuIcon}
tip="Row actions"
variant="secondary"
onClick={() => setIsOpen(!isOpen)}
/>
<PopoverContainer
alignment="bottom-left"
allowPageInteraction
closeOnViewportExit
isOpen={isOpen}
offset={0}
targetRef={ref}
onRequestClose={() => setIsOpen(false)}
>
<Menu borderRadius="md" spacing="normal" variant="popover">
<MenuItem onClick={() => setIsOpen(false)}>Edit</MenuItem>
<MenuItem
onClick={() => {
setIsOpen(false);
setIsModalOpen(true);
}}
>
View details
</MenuItem>
</Menu>
</PopoverContainer>
<Modal
isOpen={isModalOpen}
size="small"
title="Details"
onRequestClose={() => setIsModalOpen(false)}
>
{/* detail content */}
</Modal>
</Box>
);
};
const columns = [
{ key: 'name', header: 'Name', size: 'md', type: 'header' },
{
key: 'id',
header: '',
size: 'sm',
type: 'control',
justify: 'right',
render: (row) => <RowActions rowId={row.id} />,
},
];
```
Key points:
- `closeOnViewportExit` on `PopoverContainer` closes the menu when its row scrolls out of view.
- `allowPageInteraction` lets users interact with the table while the menu is open.
- Modals opened from menu items render at `zIndex={3}` by default, above the table header — this only holds because the row menu's `PopoverContainer` stays `inline`; see [`gamut-z-index`](../gamut-z-index/SKILL.md) for why a portalling menu would break this.
## Scrollable table
`scrollable` defaults to `true` on DataTable. The first `type: 'header'` column sticks to the left. Add `shadow` for a visual overflow indicator.
```tsx
<DataTable id="wide-table" idKey="id" columns={columns} rows={rows} shadow />
```
## Empty state
DataTable shows a default empty state when `rows` is empty. Override with `emptyMessage`. For the `tbody > tr > th` layout pattern required for valid table semantics, see [`gamut-datalist` — Empty state](../gamut-datalist/SKILL.md#empty-state).
## Loading state
Pass `loading` to replace row content with shimmer placeholders while data fetches.
```tsx
<DataTable id="my-table" idKey="id" columns={columns} rows={[]} loading />
```
## Color mode
DataTable inherits background color from the `current-background` token. Wrap in `Background` from `@codecademy/gamut-styles` to apply a surface color and automatically switch to dark mode contrast.
```tsx
import { Background } from '@codecademy/gamut-styles';
<Background bg="black" p={8}>
<DataTable id="dark-table" idKey="id" rows={data} columns={columns} />
</Background>;
```
## Container queries
DataTable uses CSS container queries by default for responsive column stacking. Pass `disableContainerQuery` when the table lives in a constrained container or you are managing your own responsive logic — same pattern as [`gamut-datalist`](../gamut-datalist/SKILL.md#container-queries).
## Accessibility
- DataTable renders as a semantic `<table>` element automatically.
- Sort controls receive `aria-sort` automatically when `sortable` is set on a column.
- Filter controls are keyboard-accessible via the column header dropdowns.
- For custom `emptyMessage`, use `tbody > tr > th` structure for valid table semantics (see empty state example above).