| name | gamut-list |
| description | Use this skill when building list or table layouts with List, ListRow, ListCol, and TableHeader — including variant and spacing selection, expandable row patterns, ordered/table layouts, and the rule that a list of disclosure-style items must use List's expandable row pattern (not multiple Disclosure components). See gamut-accessibility for ARIA and focus guidance. |
Gamut List
Structured, repeating layouts built from List, ListRow, ListCol, and TableHeader. Colors, borders, and spacing are wired through the variant and spacing props — consumers do not override these with raw CSS values.
Source: @codecademy/gamut — packages/gamut/src/List/List.tsx
See also: gamut-datatable — use instead of List when data needs sorting, filtering, or query state. gamut-datalist — use instead of List when rows need expansion or selection. gamut-accessibility — ARIA, focus, and keyboard interaction rules. gamut-layout — spacing tokens and system props.
Storybook:
Components
import { List, ListRow, ListCol, TableHeader } from '@codecademy/gamut';
| Component | Role |
|---|
List | Root wrapper; sets variant, spacing, as, and context for children |
ListRow | Single row; handles expandable content and click interactions |
ListCol | Column cell; controls type, size, fill, and justification |
TableHeader | Sticky header row; use only with List as="table" |
When to use List
List is for fully custom, manually composed layouts. Reach for a higher-level component first:
- Data that needs sorting, filtering, or query state → use
DataTable or DataList instead of wiring these up manually in List.
- Rows that need expansion or row selection → use
DataList.
Use List directly when:
- You need fully custom row/column composition that DataTable or DataList cannot accommodate.
- Displaying repetitive content where individual rows may contain interactive elements, metrics, or controls — use List, not Card.
- Comparing data across rows with a fully custom layout — use
variant="table" (or as="table") rather than a plain <table>.
- Needing numbered rows — use
as="ol".
- Needing multiple expandable/disclosure-style items — use List's expandable row pattern (see Expandable rows), not multiple standalone
Disclosure components.
Variants
<List variant="default" />
variant | Use for |
|---|
default | Rows with abstract content (buttons, custom renders); bordered, no gutter |
table | Metrics or comparable data; alternating row backgrounds |
card | Content that doesn't need to be adjacent (e.g. curriculum progress); bordered rows with vertical gutter |
block | Feature-forward designs or page scaffolding; always on a colored background |
plain | Minimal styling — no borders or backgrounds; apply custom styles per row via Emotion styled |
Spacing
<List spacing="condensed" />
spacing | Use for |
|---|
normal | Mixed content that needs room for components |
condensed | Default choice; reduced padding between rows |
compact | Tightest layout; data-dense views |
as prop
Default is ul. Pass as="ol" for numbered rows or as="table" for semantic table output.
<List as="ol">
<ListRow>
<ListCol type="header">Step one</ListCol>
<ListCol>Details</ListCol>
</ListRow>
</List>
<List as="table">
<TableHeader>
<ListCol columnHeader>Name</ListCol>
<ListCol columnHeader>Role</ListCol>
</TableHeader>
<ListRow>
<ListCol type="header">Worf</ListCol>
<ListCol>Lieutenant Commander</ListCol>
</ListRow>
</List>
Key props
List
| Prop | Type | Default | Effect |
|---|
variant | 'default' | 'table' | 'card' | 'block' | 'plain' | 'default' | Row styling |
spacing | 'normal' | 'condensed' | 'compact' | 'normal' | Row padding |
as | 'ul' | 'ol' | 'table' | 'ul' | Rendered element and semantic meaning |
header | React.ReactNode | — | Node rendered above the row list |
emptyMessage | React.ReactNode | — | Shown when children is empty |
loading | boolean | — | Shows placeholder while data loads |
scrollable | boolean | false | Enables horizontal scroll with sticky first column |
shadow | boolean | false | Right-side shadow when scrollable content overflows |
disableContainerQuery | boolean | false | Falls back to media queries instead of container queries |
rowBreakpoint | 'xs' | 'sm' | 'md' | 'xs' | Breakpoint at which columns stack |
ListRow
| Prop | Type | Notes |
|---|
expanded | boolean | Required when renderExpanded is set |
renderExpanded | () => React.ReactNode | Content revealed when expanded is true; animates in/out |
expandedRowAriaLabel | string | aria-label for the revealed region |
keepSpacingWhileExpanded | boolean | Maintains row spacing while content is expanded |
onClick | mouse event handler | Makes the full row interactive (adds role="button", keyboard support) |
ListCol type
type | Use for |
|---|
header | Primary label column; sticky when List is scrollable |
content | Secondary text content |
control | Action controls (buttons, menus) |
expand | Expanded content area |
expandControl | The toggle button column (no right-padding) |
select | Checkbox / selection column |
ListCol size
'content' (default, fits content) | 'sm' | 'md' | 'lg' | 'xl'
Pass fill to grow a column to fill remaining space.
Expandable rows
List provides two patterns for rows that reveal content. Both animate open/closed via framer-motion.
Expand on button click
Use ExpandControl in a type="expandControl" column to toggle a specific row.
const [isExpanded, setExpanded] = useState(false);
<ListRow
expanded={isExpanded}
renderExpanded={() => <Text>Revealed content</Text>}
expandedRowAriaLabel="Row details"
>
<ListCol type="header">Row label</ListCol>
<ListCol type="content">Secondary detail</ListCol>
<ListCol type="expandControl">
<ExpandControl
expanded={isExpanded}
onExpand={() => setExpanded(!isExpanded)}
/>
</ListCol>
</ListRow>;
Expand on row click
Pass onClick to ListRow to make the entire row the toggle target. The row receives role="button" and keyboard Enter support automatically.
const [isExpanded, setExpanded] = useState(false);
<ListRow
expanded={isExpanded}
onClick={() => setExpanded(!isExpanded)}
renderExpanded={() => <Text>Revealed content</Text>}
>
<ListCol type="header">Row label</ListCol>
<ListCol type="content">Secondary detail</ListCol>
<ListCol type="control">
<Rotation rotated={isExpanded}>
<ArrowChevronDownIcon color="text-disabled" />
</Rotation>
</ListCol>
</ListRow>;
List of disclosure-style items
When you need multiple expandable items (an FAQ, an accordion, a list of sections), use List's expandable row pattern above — do not render multiple standalone Disclosure components.
Disclosure is designed for a single, isolated expandable container. For two or more expandable items, the correct pattern is List + ListRow with expanded / renderExpanded:
<List variant="default" spacing="normal">
{items.map(({ id, title, body }) => {
const [isExpanded, setExpanded] = useState(false);
return (
<ListRow
key={id}
expanded={isExpanded}
renderExpanded={() => <Box p={16}>{body}</Box>}
expandedRowAriaLabel={`${title} details`}
>
<ListCol type="header" fill>{title}</ListCol>
<ListCol type="expandControl">
<ExpandControl
expanded={isExpanded}
onExpand={() => setExpanded(!isExpanded)}
/>
</ListCol>
</ListRow>
);
})}
</List>
<Disclosure heading="Item 1" body="..." />
<Disclosure heading="Item 2" body="..." />
Empty state and loading
<List emptyMessage={<Text>No results found.</Text>}>
{rows}
</List>
<List loading>{rows}</List>
Scrollable layout
Use scrollable when a list has many columns and collapsing would lose information. The first (type="header") column sticks to the left.
<List scrollable shadow>
<TableHeader>
<ListCol type="header" columnHeader>
Name
</ListCol>
<ListCol size="md" columnHeader>
Score
</ListCol>
<ListCol size="md" columnHeader>
Progress
</ListCol>
</TableHeader>
{rows}
</List>
Container queries
By default List uses CSS container queries for responsive column stacking. Disable only when:
- The
List lives in a container narrower than its breakpoint
- You are managing your own responsive logic
- The list has very few rows and container query overhead is unwanted
<List disableContainerQuery spacing="condensed">
{rows}
</List>
Accessibility
ListRow sets aria-live="polite" automatically when renderExpanded is present — do not add a duplicate live region.
- Pass
expandedRowAriaLabel to label the revealed role="region".
onClick on ListRow adds role="button" and keyboard Enter support automatically.
- For sortable columns pass
aria-sort on the relevant ListCol.
- Ordered lists (
as="ol") must include at least one ListCol type="header" per row for numbering to render correctly.