Import
import { Table } from '@contentful/f36-components';// orimport { Table } from '@contentful/f36-table';
Examples
Basic usage
function TableBasicUsageExample() {return (<Table><Table.Head><Table.Row><Table.Cell>Name</Table.Cell><Table.Cell>Email</Table.Cell><Table.Cell>Organization role</Table.Cell><Table.Cell>Last activity</Table.Cell></Table.Row></Table.Head><Table.Body><Table.Row><Table.Cell>Claus Mitchell</Table.Cell><Table.Cell>claus.mitchell@contentful.com</Table.Cell><Table.Cell>CEO</Table.Cell><Table.Cell>August 29, 2018</Table.Cell></Table.Row><Table.Row><Table.Cell>Johannes Ramos</Table.Cell><Table.Cell>johannes.ramos@contentful.com</Table.Cell><Table.Cell>CTO</Table.Cell><Table.Cell>July 27, 2019</Table.Cell></Table.Row><Table.Row><Table.Cell>Alex Kalinoski</Table.Cell><Table.Cell>alex.kalinoski@contentful.com</Table.Cell><Table.Cell>CDO</Table.Cell><Table.Cell>June 13, 2019</Table.Cell></Table.Row></Table.Body></Table>);}
Dynamic creation
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 Table.Row and Table.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 (<Table><Table.Head><Table.Row><Table.Cell>Name</Table.Cell><Table.Cell>Description</Table.Cell><Table.Cell>Updated</Table.Cell><Table.Cell>Status</Table.Cell></Table.Row></Table.Head><Table.Body>{contentTypes.map((contentType) => {return (<Table.Row key={contentType.id}><Table.Cell>{contentType.name || 'Untitled'}</Table.Cell><Table.Cell>{contentType.description}</Table.Cell><Table.Cell>{contentType.updatedAt}</Table.Cell><Table.Cell><Badgevariant={contentType.status === 'published' ? 'positive' : 'warning'}>{contentType.status}</Badge></Table.Cell></Table.Row>);})}</Table.Body></Table>);}
With sorting
Table cells in the table header can be marked as sortable and sorted either in ascending or descending order.
function TableWithSorting() {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',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',},];const [sorting, setSorting] = useState(undefined);const [sortedRows, setSortedRows] = useState(contentTypes);const handleSort = ({ column }) => {const direction =sorting && sorting.column === column? sorting.direction === TableCellSorting.Ascending? TableCellSorting.Descending: TableCellSorting.Ascending: TableCellSorting.Ascending;setSortedRows((rows) => {const sorted = rows.sort((rowA, rowB) => {let a = rowA[column];const b = rowB[column];if (column === 'name') {a = rowA[column] || 'Untitled';}return a.localeCompare(b);});return direction === TableCellSorting.Ascending? sorted: sorted.reverse();});setSorting({ column, direction });};return (<Table><Table.Head><Table.Row><Table.CellisSortableonClick={() => handleSort({ column: 'name' })}sortDirection={sorting && sorting.column === 'name' ? sorting.direction : false}>Name</Table.Cell><Table.Cell>Description</Table.Cell><Table.CellisSortableonClick={() => handleSort({ column: 'updatedAt' })}sortDirection={sorting && sorting.column === 'updatedAt'? sorting.direction: false}>Updated</Table.Cell><Table.Cell>Status</Table.Cell></Table.Row></Table.Head><Table.Body>{sortedRows.map((contentType) => {return (<Table.Row key={contentType.id}><Table.Cell>{contentType.name || 'Untitled'}</Table.Cell><Table.Cell>{contentType.description}</Table.Cell><Table.Cell>{contentType.updatedAt}</Table.Cell><Table.Cell><Badgevariant={contentType.status === 'published' ? 'positive' : 'warning'}>{contentType.status}</Badge></Table.Cell></Table.Row>);})}</Table.Body></Table>);}
Scrollable
Use layout="scrollable" for tables with many columns or content that should not be compressed on smaller viewports. The table keeps its column layout and allows horizontal scrolling when the available width is not enough.
const withLongContentTableData = Array.from({ length: 15 }, (_, 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',},] as const;const entry = entries[index % entries.length];return {...entry,id: `entry-${index + 1}`,updated: `${index + 1} days ago`,};});function TableScrollableExample() {return (<Table layout="scrollable"><Table.Head><Table.Row><Table.Cell>Name</Table.Cell><Table.Cell>Status</Table.Cell><Table.Cell>Content type</Table.Cell><Table.Cell>Updated by</Table.Cell><Table.Cell>Updated</Table.Cell><Table.Cell>Locale</Table.Cell><Table.Cell>Space</Table.Cell><Table.Cell>Environment</Table.Cell><Table.Cell>Tags</Table.Cell><Table.Cell>ID</Table.Cell></Table.Row></Table.Head><Table.Body>{withLongContentTableData.map((item) => (<Table.Row key={item.id}><Table.Cell>{item.name}</Table.Cell><Table.Cell><EntityStatusBadge entityStatus={item.status} /></Table.Cell><Table.Cell>{item.contentType}</Table.Cell><Table.Cell>{item.updatedBy}</Table.Cell><Table.Cell>{item.updated}</Table.Cell><Table.Cell>{item.locale}</Table.Cell><Table.Cell>{item.space}</Table.Cell><Table.Cell>{item.environment}</Table.Cell><Table.Cell>{item.tags}</Table.Cell><Table.Cell>{item.id}</Table.Cell></Table.Row>))}</Table.Body></Table>);}
Scrollable with sticky first column
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.
const withLongContentTableData = Array.from({ length: 15 }, (_, 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',},] as const;const entry = entries[index % entries.length];return {...entry,id: `entry-${index + 1}`,updated: `${index + 1} days ago`,};});function TableScrollableStickyFirstColumnExample() {return (<Table layout="scrollable" isFirstColumnSticky><Table.Head><Table.Row><Table.Cell>Name</Table.Cell><Table.Cell>Status</Table.Cell><Table.Cell>Content type</Table.Cell><Table.Cell>Updated by</Table.Cell><Table.Cell>Updated</Table.Cell><Table.Cell>Locale</Table.Cell><Table.Cell>Space</Table.Cell><Table.Cell>Environment</Table.Cell><Table.Cell>Tags</Table.Cell><Table.Cell>ID</Table.Cell></Table.Row></Table.Head><Table.Body>{withLongContentTableData.map((item) => (<Table.Row key={item.id}><Table.Cell>{item.name}</Table.Cell><Table.Cell><EntityStatusBadge entityStatus={item.status} /></Table.Cell><Table.Cell>{item.contentType}</Table.Cell><Table.Cell>{item.updatedBy}</Table.Cell><Table.Cell>{item.updated}</Table.Cell><Table.Cell>{item.locale}</Table.Cell><Table.Cell>{item.space}</Table.Cell><Table.Cell>{item.environment}</Table.Cell><Table.Cell>{item.tags}</Table.Cell><Table.Cell>{item.id}</Table.Cell></Table.Row>))}</Table.Body></Table>);}
Scrollable with sticky header and first column
For large tables that need both horizontal and vertical scrolling, combine layout="scrollable", isFirstColumnSticky, and Table.Head isSticky. Place the table in a height-constrained container to avoid double scrollbars coming from the outer container.
const withLongContentTableData = Array.from({ length: 15 }, (_, 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',},] as const;const entry = entries[index % entries.length];return {...entry,id: `entry-${index + 1}`,updated: `${index + 1} days ago`,};});function TableScrollableStickyHeaderAndFirstColumnExample() {return (<divstyle={{display: 'flex',flexDirection: 'column',height: '320px',overflowY: 'auto',}}><Table layout="scrollable" isFirstColumnSticky><Table.Head isSticky><Table.Row><Table.Cell>Name</Table.Cell><Table.Cell>Status</Table.Cell><Table.Cell>Content type</Table.Cell><Table.Cell>Updated by</Table.Cell><Table.Cell>Updated</Table.Cell><Table.Cell>Locale</Table.Cell><Table.Cell>Space</Table.Cell><Table.Cell>Environment</Table.Cell><Table.Cell>Tags</Table.Cell><Table.Cell>ID</Table.Cell></Table.Row></Table.Head><Table.Body>{withLongContentTableData.map((item) => (<Table.Row key={item.id}><Table.Cell>{item.name}</Table.Cell><Table.Cell><EntityStatusBadge entityStatus={item.status} /></Table.Cell><Table.Cell>{item.contentType}</Table.Cell><Table.Cell>{item.updatedBy}</Table.Cell><Table.Cell>{item.updated}</Table.Cell><Table.Cell>{item.locale}</Table.Cell><Table.Cell>{item.space}</Table.Cell><Table.Cell>{item.environment}</Table.Cell><Table.Cell>{item.tags}</Table.Cell><Table.Cell>{item.id}</Table.Cell></Table.Row>))}</Table.Body></Table></div>);}
Props (API reference)
Open in StorybookTable
Name | Type | Default |
|---|---|---|
| className | string CSS class to be appended to the root element | |
| isFirstColumnSticky | false true | false |
| layout | "inline" "embedded" "scrollable" | inline |
| testId | string A [data-test-id] attribute used for testing purposes | cf-ui-table |
| verticalAlign | "baseline" "bottom" "middle" "top" | top |
Table.Head
Name | Type | Default |
|---|---|---|
| children required | ReactNode | |
| className | string CSS class to be appended to the root element | |
| isSticky | false true | false |
| offsetTop | string number | |
| testId | string A [data-test-id] attribute used for testing purposes | cf-ui-table-head |
Table.Body
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 |
Table.Row
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 |
Table.Cell
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 Aria label for the sort button when isSortable is set | |
| sortDirection | "ascending" "descending" | |
| testId | string A [data-test-id] attribute used for testing purposes | |
| width | string number |
Content guidelines
- Keep headers short
- Headers should be informative and descriptive
- Content in the table should be concise and scannable
Accessibility
- It will render tabular data using the native HTML element
tablewhich is recommended.