Alpha component
TableNext component is subject to major changes and is for experimentation purposes only. Not recommended for use in production software.
TableNext component is subject to major changes and is for experimentation purposes only. Not recommended for use in production software.
TableNext is the next iteration of the basic Table component. It is per default responsive by receiving a horizontal scrollbar. It additionally offers a second responsive layout mode called stackable. In the stackable layout, the Table data will be displayed in a grid, each table row represented as a single unit with all cells of this row stacked on top of each other. If a table header with titles is required, the column title is displayed next to the table cell.
This component is still in an unstable alpha state. The layout="stackable" variant has the following restrictions:
isSortable, sortDirection, sortButtonAriaLabel) have no effect in stackable mode. Use layout="scrollable" for sortable tables.columnTitles accepts strings. Custom React elements — checkboxes, tooltips, icons, badges, or any other component — cannot be used as row labels in the stacked view.TableNext.Cell within TableNext.Head are not supported in stackable mode.Support for richer head cell content, sorting, and check-all checkboxes in stackable tables is planned for a future release.
import { TableNext } from '@contentful/f36-table-next';
function TableBasicUsageExample() {return (<TableNext><TableNext.Head><TableNext.Row><TableNext.Cell>Name</TableNext.Cell><TableNext.Cell>Email</TableNext.Cell><TableNext.Cell>Organization role</TableNext.Cell><TableNext.Cell>Last activity</TableNext.Cell></TableNext.Row></TableNext.Head><TableNext.Body><TableNext.Row><TableNext.Cell>Claus Mitchell</TableNext.Cell><TableNext.Cell>claus.mitchell@contentful.com</TableNext.Cell><TableNext.Cell>CEO</TableNext.Cell><TableNext.Cell>August 29, 2018</TableNext.Cell></TableNext.Row><TableNext.Row><TableNext.Cell>Johannes Ramos</TableNext.Cell><TableNext.Cell>johannes.ramos@contentful.com</TableNext.Cell><TableNext.Cell>CTO</TableNext.Cell><TableNext.Cell>July 27, 2019</TableNext.Cell></TableNext.Row><TableNext.Row><TableNext.Cell>Alex Kalinoski</TableNext.Cell><TableNext.Cell>alex.kalinoski@contentful.com</TableNext.Cell><TableNext.Cell>CDO</TableNext.Cell><TableNext.Cell>June 13, 2019</TableNext.Cell></TableNext.Row></TableNext.Body></TableNext>);}
One very common use case for a table is that you will have a set of data and you would like to show a table row for each item in that set.
To achieve that result, you can iterate over the data and create TableNext.Row and TableNext.Cell for each item:
function TableDynamicCreation() {const contentTypes = [{id: '1',name: 'Category',description:'Categories can be applied to Courses and Lessons. Assigning Multiple categories is also possible.',updatedAt: 'Nov 15, 2021',status: 'published',},{id: '2',updatedAt: 'Nov 15, 2021',status: 'draft',},{id: '3',name: 'Layout',description:'A page consisting of freely configurable and rearrangeable content modules.',updatedAt: 'Nov 15, 2021',status: 'published',},];return (<TableNext><TableNext.Head><TableNext.Row><TableNext.Cell>Name</TableNext.Cell><TableNext.Cell>Description</TableNext.Cell><TableNext.Cell>Updated</TableNext.Cell><TableNext.Cell>Status</TableNext.Cell></TableNext.Row></TableNext.Head><TableNext.Body>{contentTypes.map((contentType) => {return (<TableNext.Row key={contentType.id}><TableNext.Cell>{contentType.name || 'Untitled'}</TableNext.Cell><TableNext.Cell>{contentType.description}</TableNext.Cell><TableNext.Cell>{contentType.updatedAt}</TableNext.Cell><TableNext.Cell><Badgevariant={contentType.status === 'published' ? 'positive' : 'warning'}>{contentType.status}</Badge></TableNext.Cell></TableNext.Row>);})}</TableNext.Body></TableNext>);}
Combine layout="scrollable" with isFirstColumnSticky when the first column contains the primary identifier for each row. This keeps the row context visible while users scroll horizontally.
function TableScrollableStickyFirstColumnExample() {const withLongContentTableData = Array.from({ length: 5 }, (_, index) => {const entries = [{name: 'How to optimize images in WordPress for faster loading',status: 'published',contentType: 'Blog post',updatedBy: 'Ayman Mahmoud',locale: 'en-US',space: 'Marketing',environment: 'master',tags: 'featured, seo-optimized',},{name: 'Building accessible web applications from the ground up',status: 'changed',contentType: 'Landing page',updatedBy: 'Jane Roe',locale: 'de-DE',space: 'Documentation',environment: 'staging',tags: 'accessibility, review',},{name: 'Content modeling best practices for large teams',status: 'draft',contentType: 'Case study',updatedBy: 'John Doe',locale: 'fr-FR',space: 'Support',environment: 'development',tags: 'internal',},];const entry = entries[index % entries.length];return {...entry,id: `entry-${index + 1}`,updated: `${index + 1} days ago`,};});return (<TableNext isFirstColumnSticky><TableNext.Head><TableNext.Row><TableNext.Cell>Name</TableNext.Cell><TableNext.Cell>Status</TableNext.Cell><TableNext.Cell>Content type</TableNext.Cell><TableNext.Cell>Updated by</TableNext.Cell><TableNext.Cell>Updated</TableNext.Cell><TableNext.Cell>Locale</TableNext.Cell><TableNext.Cell>Space</TableNext.Cell><TableNext.Cell>Environment</TableNext.Cell><TableNext.Cell>Tags</TableNext.Cell><TableNext.Cell>ID</TableNext.Cell></TableNext.Row></TableNext.Head><TableNext.Body>{withLongContentTableData.map((item) => (<TableNext.Row key={item.id}><TableNext.Cell>{item.name}</TableNext.Cell><TableNext.Cell><EntityStatusBadgeentityStatus={item.status === 'published'? 'published': item.status === 'changed'? 'changed': 'draft'}/></TableNext.Cell><TableNext.Cell>{item.contentType}</TableNext.Cell><TableNext.Cell>{item.updatedBy}</TableNext.Cell><TableNext.Cell>{item.updated}</TableNext.Cell><TableNext.Cell>{item.locale}</TableNext.Cell><TableNext.Cell>{item.space}</TableNext.Cell><TableNext.Cell>{item.environment}</TableNext.Cell><TableNext.Cell>{item.tags}</TableNext.Cell><TableNext.Cell>{item.id}</TableNext.Cell></TableNext.Row>))}</TableNext.Body></TableNext>);}
For large tables that need both horizontal and vertical scrolling, combine layout="scrollable", isFirstColumnSticky, and TableNext.Head isSticky. Place the table in a height-constrained container to avoid double scrollbars coming from the outer container.
function TableScrollableStickyHeaderAndFirstColumnExample() {const withLongContentTableData = Array.from({ length: 5 }, (_, index) => {const entries = [{name: 'How to optimize images in WordPress for faster loading',status: 'published',contentType: 'Blog post',updatedBy: 'Ayman Mahmoud',locale: 'en-US',space: 'Marketing',environment: 'master',tags: 'featured, seo-optimized',},{name: 'Building accessible web applications from the ground up',status: 'changed',contentType: 'Landing page',updatedBy: 'Jane Roe',locale: 'de-DE',space: 'Documentation',environment: 'staging',tags: 'accessibility, review',},{name: 'Content modeling best practices for large teams',status: 'draft',contentType: 'Case study',updatedBy: 'John Doe',locale: 'fr-FR',space: 'Support',environment: 'development',tags: 'internal',},];const entry = entries[index % entries.length];return {...entry,id: `entry-${index + 1}`,updated: `${index + 1} days ago`,};});return (<divstyle={{display: 'flex',flexDirection: 'column',height: '320px',overflowY: 'auto',}}><TableNext isFirstColumnSticky><TableNext.Head isSticky><TableNext.Row><TableNext.Cell>Name</TableNext.Cell><TableNext.Cell>Status</TableNext.Cell><TableNext.Cell>Content type</TableNext.Cell><TableNext.Cell>Updated by</TableNext.Cell><TableNext.Cell>Updated</TableNext.Cell><TableNext.Cell>Locale</TableNext.Cell><TableNext.Cell>Space</TableNext.Cell><TableNext.Cell>Environment</TableNext.Cell><TableNext.Cell>Tags</TableNext.Cell><TableNext.Cell>ID</TableNext.Cell></TableNext.Row></TableNext.Head><TableNext.Body>{withLongContentTableData.map((item) => (<TableNext.Row key={item.id}><TableNext.Cell>{item.name}</TableNext.Cell><TableNext.Cell><EntityStatusBadgeentityStatus={item.status === 'published'? 'published': item.status === 'changed'? 'changed': 'draft'}/></TableNext.Cell><TableNext.Cell>{item.contentType}</TableNext.Cell><TableNext.Cell>{item.updatedBy}</TableNext.Cell><TableNext.Cell>{item.updated}</TableNext.Cell><TableNext.Cell>{item.locale}</TableNext.Cell><TableNext.Cell>{item.space}</TableNext.Cell><TableNext.Cell>{item.environment}</TableNext.Cell><TableNext.Cell>{item.tags}</TableNext.Cell><TableNext.Cell>{item.id}</TableNext.Cell></TableNext.Row>))}</TableNext.Body></TableNext></div>);}
Sorting is supported on non-stackable tables. Add isSortable to a head cell, pass sortDirection to reflect the active sort, and handle onClick to update state. Sorting props are not available on layout="stackable" tables.
type SortState = {column: string;direction: TableCellSorting;} | null;const contentTypes = [{id: '1',name: 'Category',description:'Categories can be applied to Courses and Lessons. Assigning Multiple categories is also possible.',updatedAt: 'Nov 1, 2021',status: 'published',},{id: '2',name: 'Untitled',updatedAt: 'Nov 11, 2021',status: 'draft',},{id: '3',name: 'Layout',description:'A page consisting of freely configurable and rearrangeable content modules.',updatedAt: 'Nov 18, 2021',status: 'published',},];function TableNextWithSortingExample() {const [sorting, setSorting] = useState<SortState>(null);const [sortedRows, setSortedRows] = useState(contentTypes);const handleSort = (column: string) => {const direction =sorting?.column === column? sorting.direction === TableCellSorting.Ascending? TableCellSorting.Descending: TableCellSorting.Ascending: TableCellSorting.Ascending;setSortedRows((rows) => {const sorted = [...rows].sort((a, b) =>(a[column as keyof typeof a] ?? '').localeCompare(b[column as keyof typeof b] ?? '',),);return direction === TableCellSorting.Ascending? sorted: sorted.reverse();});setSorting({ column, direction });};return (<TableNext><TableNext.Head><TableNext.Row><TableNext.CellisSortableonClick={() => handleSort('name')}sortDirection={sorting?.column === 'name' ? sorting.direction : undefined}>Name</TableNext.Cell><TableNext.Cell>Description</TableNext.Cell><TableNext.CellisSortableonClick={() => handleSort('updatedAt')}sortDirection={sorting?.column === 'updatedAt' ? sorting.direction : undefined}>Updated</TableNext.Cell><TableNext.Cell>Status</TableNext.Cell></TableNext.Row></TableNext.Head><TableNext.Body>{sortedRows.map((item) => (<TableNext.Row key={item.id}><TableNext.Cell>{item.name}</TableNext.Cell><TableNext.Cell>{item.description}</TableNext.Cell><TableNext.Cell>{item.updatedAt}</TableNext.Cell><TableNext.Cell><Badgevariant={item.status === 'published' ? 'positive' : 'warning'}>{item.status}</Badge></TableNext.Cell></TableNext.Row>))}</TableNext.Body></TableNext>);}
Use layout="stackable" when the table needs to remain readable in narrow containers — for example in sidebars, panels, or split-pane layouts — or when the page may be viewed on small viewports where horizontal scrolling would be disruptive. The stackable layout reflows each row into a vertically stacked block, keeping all values visible without requiring the user to scroll sideways.
Prefer layout="scrollable" when the table has many columns that need to stay in view at once, when column relationships matter for scanning across rows, or when sorting is required.
For containers smaller than the stackableBreakpoint (default 700px) the table will be presented as 90° flipped, stacking all table cells of a single table row underneath each other with column titles next to them. Pass stackableBreakpoint to override the breakpoint — numbers are treated as px, strings accept px, rem, or em.
function StackableWithTitleExample() {const stackableColumnTitles = ['Name','Email','Organization role','Last activity',];return (<TableNext layout="stackable" columnTitles={stackableColumnTitles}><TableNext.Head><TableNext.Row>{stackableColumnTitles.map((title, id) => (<TableNext.Cell key={`table-head-cell-${title}-${id}`}>{title}</TableNext.Cell>))}</TableNext.Row></TableNext.Head><TableNext.Body><TableNext.Row><TableNext.Cell>Claus Mitchell</TableNext.Cell><TableNext.Cell>claus.mitchell@contentful.com</TableNext.Cell><TableNext.Cell>CEO</TableNext.Cell><TableNext.Cell>August 29, 2018</TableNext.Cell></TableNext.Row><TableNext.Row><TableNext.Cell>Johannes Ramos</TableNext.Cell><TableNext.Cell>johannes.ramos@contentful.com</TableNext.Cell><TableNext.Cell>CTO</TableNext.Cell><TableNext.Cell>July 27, 2019</TableNext.Cell></TableNext.Row><TableNext.Row><TableNext.Cell>Alex Kalinoski</TableNext.Cell><TableNext.Cell>alex.kalinoski@contentful.com</TableNext.Cell><TableNext.Cell>CDO</TableNext.Cell><TableNext.Cell>June 13, 2019</TableNext.Cell></TableNext.Row></TableNext.Body></TableNext>);}
For containers smaller than the stackableBreakpoint (default 700px) the table will be presented as 90° flipped, stacking all table cells of a single row underneath each other. Pass stackableBreakpoint to override the breakpoint — numbers are treated as px, strings accept px, rem, or em.
function StackableExample() {return (<TableNext layout="stackable"><TableNext.Body><TableNext.Row><TableNext.Cell>Claus Mitchell</TableNext.Cell><TableNext.Cell>claus.mitchell@contentful.com</TableNext.Cell><TableNext.Cell>CEO</TableNext.Cell><TableNext.Cell>August 29, 2018</TableNext.Cell></TableNext.Row><TableNext.Row><TableNext.Cell>Johannes Ramos</TableNext.Cell><TableNext.Cell>johannes.ramos@contentful.com</TableNext.Cell><TableNext.Cell>CTO</TableNext.Cell><TableNext.Cell>July 27, 2019</TableNext.Cell></TableNext.Row><TableNext.Row><TableNext.Cell>Alex Kalinoski</TableNext.Cell><TableNext.Cell>alex.kalinoski@contentful.com</TableNext.Cell><TableNext.Cell>CDO</TableNext.Cell><TableNext.Cell>June 13, 2019</TableNext.Cell></TableNext.Row></TableNext.Body></TableNext>);}
Name | Type | Default |
|---|---|---|
| className | string CSS class to be appended to the root element | |
| columnTitles | string[] Column titles used as inline labels in stacked rows at small container sizes. Only valid with layout="stackable". The order must match the column order of your TableHead row. Only supported with layout="stackable". | |
| isFirstColumnSticky | false true | false |
| layout | "stackable" "scrollable" | scrollable |
| stackableBreakpoint | number `${number}px` `${number}rem` `${number}em` Container width at which the table switches to the stacked layout. Numbers are interpreted as px. Only available when layout is "stackable". Only supported with layout="stackable". | 700 |
| testId | string A [data-test-id] attribute used for testing purposes | cf-ui-table-next |
| variant | "inline" "embedded" | inline |
| verticalAlign | "baseline" "bottom" "middle" "top" | top |
Name | Type | Default |
|---|---|---|
| children required | ReactNode | |
| className | string CSS class to be appended to the root element | |
| testId | string A [data-test-id] attribute used for testing purposes |
Name | Type | Default |
|---|---|---|
| children required | ReactNode | |
| className | string CSS class to be appended to the root element | |
| isSelected | false true | false |
| testId | string A [data-test-id] attribute used for testing purposes | cf-ui-table-row |
Name | Type | Default |
|---|---|---|
| align | "center" "left" "right" | |
| as | "a" "abbr" "address" "animate" "animateMotion" "animateTransform" "area" "article" "aside" "audio" "b" "base" "bdi" "bdo" "big" "blockquote" "body" "br" "button" "canvas" "caption" "center" "circle" "cite" "clipPath" "code" "col" "colgroup" "data" "datalist" "dd" "defs" "del" "desc" "details" "dfn" "dialog" "div" "dl" "dt" "ellipse" "em" "embed" "feBlend" "feColorMatrix" "feComponentTransfer" "feComposite" "feConvolveMatrix" "feDiffuseLighting" "feDisplacementMap" "feDistantLight" "feDropShadow" "feFlood" "feFuncA" "feFuncB" "feFuncG" "feFuncR" "feGaussianBlur" "feImage" "feMerge" "feMergeNode" "feMorphology" "feOffset" "fePointLight" "feSpecularLighting" "feSpotLight" "feTile" "feTurbulence" "fieldset" "figcaption" "figure" "filter" "footer" "foreignObject" "form" "g" "h1" "h2" "h3" "h4" "h5" "h6" "head" "header" "hgroup" "hr" "html" "i" "iframe" "image" "img" "input" "ins" "kbd" "keygen" "label" "legend" "li" "line" "linearGradient" "link" "main" "map" "mark" "marker" "mask" "menu" "menuitem" "meta" "metadata" "meter" "mpath" "nav" "noindex" "noscript" "object" "ol" "optgroup" "option" "output" "p" "param" "path" "pattern" "picture" "polygon" "polyline" "pre" "progress" "q" "radialGradient" "rect" "rp" "rt" "ruby" "s" "samp" "script" "search" "section" "select" "set" "slot" "small" "source" "span" "stop" "strong" "style" "sub" "summary" "sup" "svg" "switch" "symbol" "table" "tbody" "td" "template" "text" "textarea" "textPath" "tfoot" "th" "thead" "time" "title" "tr" "track" "tspan" "u" "ul" "use" "var" "video" "view" "wbr" "webview" ComponentClass<any, any> FunctionComponent<any> | |
| children | ReactNode | |
| className | string CSS class to be appended to the root element | |
| isSortable | false true | |
| isTruncated | false true | |
| isWordBreak | false true | |
| sortButtonAriaLabel | string Override auto-generated aria-label on the sort button | |
| sortDirection | "ascending" "descending" | |
| testId | string A [data-test-id] attribute used for testing purposes | |
| width | string number |
table which is recommended.