| name | gamut-datalist |
| description | Use this skill when building a DataList for item-focused layouts with row expansion, row selection, or rich per-item content — including expandedContent, onRowSelect, onRowExpand, variant (default/card), useLocalQuery, and empty/loading states. Do not use for bulk data comparison tables (see gamut-datatable), or for small static lists (see gamut-list). |
Gamut DataList
Item-focused list for managing, engaging with, and expanding individual rows. Use when users interact with items — opening details, selecting for bulk actions, or viewing expanded layouts — rather than scanning and comparing data across rows.
Source: @codecademy/gamut — packages/gamut/src/DataList/DataList.tsx
See also: gamut-datatable — query-focused table for bulk data comparison. gamut-list — lower-level list primitives for fully custom layouts. gamut-accessibility — ARIA and keyboard interaction.
Storybook: Organisms / Lists & Tables / DataList
Components
import { DataList } from '@codecademy/gamut';
import { useLocalQuery } from '@codecademy/gamut';
| Symbol | Role |
|---|
DataList | Root component; supports expansion, selection, and card/default variants |
useLocalQuery | Client-side hook for sort/filter state; spread its return value directly into DataList |
When to use DataList
- Users open, expand, or engage with individual items (content libraries, assignment lists, course catalogs).
- Rows need expandable detail panels with rich layouts.
- Users need to select rows for bulk actions.
- Items contain icons, graphics, or complex layouts rather than plain metrics.
- Optional filtering or sorting across shared attributes is needed at the list level.
Do not use DataList when:
- The goal is to compare data across rows (scores, metrics, statuses in columns) → use
DataTable.
- The list is small, static, and needs fully custom row layouts → use
List directly.
- You need a table with horizontal scrolling → use
DataTable (DataList is not scrollable).
Design principles
- Engage with individual items: design each row to support the action users take on it (open, launch, select, expand).
- Customize items with rich content: icons, progress indicators, graphics, and other atoms are appropriate within rows.
- Keep item controls visible: action controls should be on the right side of the row.
- Place lists inside main containers: avoid overflow by placing DataList in appropriately sized parent layouts.
- Use DataTable for comparison-first designs: if the design is primarily about scanning data between items, reach for DataTable instead.
Variants
<DataList variant="default" />
variant | Use for |
|---|
default | Standard bordered rows; best for most item-management layouts |
card | Rows with vertical gap; best for content that doesn't need to be visually adjacent |
Props
| Prop | Type | Default | Notes |
|---|
id | string | required | Unique ID for the list |
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 |
onQueryChange | OnQueryChange<Row> | — | Called when sort or filter changes |
selected | Row[IdKey][] | — | Array of selected row IDs |
onRowSelect | RowStateChange<'select' | 'select-all', Row[IdKey]> | — | Called on row or select-all toggle |
expanded | Row[IdKey][] | — | Array of expanded row IDs |
onRowExpand | RowStateChange<'expand', Row[IdKey]> | — | Called when a row is expanded or collapsed |
expandedContent | ExpandRow<Row> | — | Render function for expanded row content; receives { row, onCollapse } |
variant | 'default' | 'card' | 'default' | Row visual style |
header | boolean | — | Whether to show a header row |
hideSelectAll | boolean | false | Hides the select-all checkbox in the header |
loading | boolean | false | Replaces row content with shimmer placeholders |
spacing | 'condensed' | 'normal' | 'condensed' | Row padding |
emptyMessage | ReactNode | default empty state | Rendered when rows is empty |
disableContainerQuery | boolean | false | Falls back to media queries instead of container queries |
DataList always sets scrollable={false}. For a horizontally scrollable table, use DataTable.
ColumnConfig
Identical to DataTable — see gamut-datatable for the full table.
Basic usage
import { DataList, useLocalQuery } from '@codecademy/gamut';
const columns = [
{ key: 'title', header: 'Title', size: 'md', type: 'header', fill: true },
{ key: 'status', header: 'Status', size: 'sm' },
];
const MyList = ({ data }) => {
const queryData = useLocalQuery({ idKey: 'id', rows: data, columns });
return <DataList id="my-list" idKey="id" columns={columns} {...queryData} />;
};
Expandable rows
Pass expandedContent, expanded, and onRowExpand together. The expandedContent render function receives { row, onCollapse } — call onCollapse from within the expanded panel to close it programmatically.
const [expanded, setExpanded] = useState<string[]>([]);
const handleExpand = ({ type, payload: { rowId, toggle } }) => {
if (type === 'expand') {
setExpanded((prev) =>
toggle ? prev.filter((id) => id !== rowId) : [...prev, rowId]
);
}
};
<DataList
id="expandable-list"
idKey="id"
columns={columns}
rows={data}
expanded={expanded}
onRowExpand={handleExpand}
expandedContent={({ row, onCollapse }) => (
<Box p={16}>
<Text>{row.title}: additional details here</Text>
<TextButton onClick={onCollapse}>Collapse</TextButton>
</Box>
)}
/>;
Selectable rows
Pass selected and onRowSelect to enable checkboxes. The callback receives { type, payload: { rowId, toggle } } where type is 'select' or 'select-all'.
const [selected, setSelected] = useState<string[]>([]);
const handleSelect = ({ type, payload: { rowId, toggle } }) => {
if (type === 'select-all') {
setSelected((prev) => (prev.length > 0 ? [] : data.map((r) => r.id)));
} else if (type === 'select') {
setSelected((prev) =>
toggle ? prev.filter((id) => id !== rowId) : [...prev, rowId]
);
}
};
<DataList
id="selectable-list"
idKey="id"
columns={columns}
rows={data}
selected={selected}
onRowSelect={handleSelect}
/>;
To hide row checkboxes entirely, omit onRowSelect and selected. To hide only the select-all checkbox, pass hideSelectAll.
Expansion + selection combined
Both can be active at once — pass all four props together.
<DataList
id="full-list"
idKey="id"
columns={columns}
rows={data}
selected={selected}
onRowSelect={handleSelect}
expanded={expanded}
onRowExpand={handleExpand}
expandedContent={({ row }) => <ExpandedDetail row={row} />}
/>
Sorting and filtering with useLocalQuery
useLocalQuery handles client-side sort and filter state. Spread its return value directly into DataList. Mark columns as sortable or provide filters in the column config.
const columns = [
{ key: 'title', header: 'Title', size: 'md', sortable: true },
{
key: 'type',
header: 'Type',
size: 'sm',
filters: ['Course', 'Path', 'Project'],
},
];
const queryData = useLocalQuery({ idKey: 'id', rows: data, columns });
<DataList id="my-list" idKey="id" columns={columns} {...queryData} />;
For server-side filtering or pagination, manage query and onQueryChange externally instead of using useLocalQuery.
Empty state
DataList shows a default empty state when rows is empty. Override with emptyMessage.
<DataList
id="my-list"
idKey="id"
columns={columns}
rows={[]}
emptyMessage={
<Box as="tbody" height="100%" width="100%">
<FlexBox
as="tr"
height="inherit"
position="absolute"
width="inherit"
zIndex={1}
>
<FlexBox
as="th"
center
column
left="calc(50% - 115px)"
p={16}
position="sticky"
top="calc(50% - 115px)"
width="fit-content"
>
<Text>No items yet.</Text>
</FlexBox>
</FlexBox>
</Box>
}
/>
Loading state
Pass loading to replace row content with shimmer placeholders while data fetches.
<DataList id="my-list" idKey="id" columns={columns} rows={[]} loading />
Container queries
DataList uses CSS container queries by default. Disable only when the list lives in a constrained container or you are managing your own responsive logic.
import { Box, DataList } from '@codecademy/gamut';
<Box width={280}>
<DataList
disableContainerQuery
id="sidebar-list"
idKey="id"
rows={data}
columns={columns}
/>
</Box>;
Accessibility
- DataList renders as a semantic
<table> via the underlying List as="table".
- Expanded rows receive an
aria-live="polite" region automatically — do not add a duplicate live region.
- The expanded content region is labeled by the row's header column text; provide meaningful header column values.
- Selection checkboxes are grouped as a checkbox list; the select-all checkbox is in the
<thead>.
- For custom
emptyMessage, use tbody > tr > th structure for valid table semantics.