Table

Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.

  • Install
    npm install @pluralsight/ps-design-system-table
  • Import
    import Table from '@pluralsight/ps-design-system-table'

Examples

Basic table

import { layout } from '@pluralsight/ps-design-system-core'
import Avatar from '@pluralsight/ps-design-system-avatar'
import Button from '@pluralsight/ps-design-system-button'
import { MoreIcon } from '@pluralsight/ps-design-system-icon'
import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
const Example: React.FC = props => {
const [users] = React.useState([
{ firstName: 'Lucy', lastName: 'Peck', email: '[email protected]' },
{ firstName: 'Jayden', lastName: 'Morales', email: '[email protected]' },
{ firstName: 'Milton', lastName: 'Lane', email: '[email protected]' }
])
return (
<Table>
<Table.Head>
<Table.Row>
<Table.Header role="columnheader" scope="col">
First name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Last name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Email
</Table.Header>
<Table.Header role="columnheader" scope="col"></Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
{users.map((user, i) => (
<Table.Row key={i}>
<Table.Header role="rowheader" scope="row">
<FlexContainer>
<Avatar alt="avatar" name={user.firstName} size="xSmall" />
<HorzSpacer />
<span>{user.firstName}</span>
</FlexContainer>
</Table.Header>
<Table.Cell>{user.lastName}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
<Table.Cell align="right">
<Button
icon={<MoreIcon />}
appearance={Button.appearances.flat}
size={Button.sizes.small}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div
style={{
display: 'inline-block',
width: layout.spacingSmall
}}
{...props}
/>
)
export default Example

Table with drawers

import { layout } from '@pluralsight/ps-design-system-core'
import Avatar from '@pluralsight/ps-design-system-avatar'
import Button from '@pluralsight/ps-design-system-button'
import { CaretDownIcon, CaretRightIcon, MoreIcon } from '@pluralsight/ps-design-system-icon'
import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
const Example: React.FC = props => {
const [users] = React.useState([
{ id: '1', firstName: 'Lucy', lastName: 'Peck', email: '[email protected]' },
{ id: '2', firstName: 'Jayden', lastName: 'Morales', email: '[email protected]' },
{ id: '3', firstName: 'Milton', lastName: 'Lane', email: '[email protected]' }
])
const initialState = [users[1].id]
const [expandedIds, setExpandedIds] = React.useState(initialState)
const expand = (user: { id: string }) => {
const next = expandedIds.concat(user.id)
setExpandedIds(next)
}
const collapse = (user: { id: string }) => {
const next = expandedIds.filter(str => str !== user.id)
setExpandedIds(next)
}
return (
<Table>
<Table.Head>
<Table.Row>
<Table.Cell />
<Table.Header role="columnheader" scope="col">
First name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Last name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Email
</Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
{users.map((user, i) => {
const expanded = expandedIds.includes(user.id)
const toggle = expanded ? collapse : expand
return (
<React.Fragment key={i}>
<Table.Row>
<Table.Cell>
<ExpandButton
expanded={expanded}
onClick={() => toggle(user)}
/>
</Table.Cell>
<Table.Cell>{user.firstName}</Table.Cell>
<Table.Cell>{user.lastName}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
</Table.Row>
<Table.Drawer expanded={expanded} colSpan={4}>
<div>
Drawer Content
</div>
</Table.Drawer>
</React.Fragment>
)
})}
</Table.Body>
</Table>
)
}
interface ExpandButtonProps extends React.ComponentProps<typeof Button> {
expanded: boolean
}
const ExpandButton: React.FC<ExpandButtonProps> = props => {
const {
appearance = 'flat',
expanded,
size = 'xSmall',
title = 'Expand/Collapse additional content',
...rest
} = props
const icon = expanded ? <CaretDownIcon /> : <CaretRightIcon />
return (
<Button
appearance={appearance}
icon={icon}
size={size}
title={title}
{...rest}
/>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div
style={{
display: 'inline-block',
width: layout.spacingSmall
}}
{...props}
/>
)
export default Example

Sticky headers

Column headers can be stuck relative to a scrollable container.

import { layout } from '@pluralsight/ps-design-system-core'
import Avatar from '@pluralsight/ps-design-system-avatar'
import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
const Example: React.FC = props => {
const [users] = React.useState([
{ firstName: 'Lucy', lastName: 'Peck', email: '[email protected]' },
{ firstName: 'Jayden', lastName: 'Morales', email: '[email protected]' },
{ firstName: 'Milton', lastName: 'Lane', email: '[email protected]' },
{ firstName: 'Lucy', lastName: 'Peck', email: '[email protected]' },
{ firstName: 'Jayden', lastName: 'Morales', email: '[email protected]' },
{ firstName: 'Milton', lastName: 'Lane', email: '[email protected]' }
])
return (
<div style={{ height: 200 }}>
<Table scrollable>
<Table.Head>
<Table.Row>
<Table.Header role="columnheader" scope="col" sticky>
First name
</Table.Header>
<Table.Header role="columnheader" scope="col" sticky>
Last name
</Table.Header>
<Table.Header role="columnheader" scope="col" sticky>
Email
</Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
{users.map((user, i) => (
<Table.Row key={i}>
<Table.Header role="rowheader" scope="row">
<FlexContainer>
<Avatar alt="avatar" name={user.firstName} size="xSmall" />
<HorzSpacer />
<span>{user.firstName}</span>
</FlexContainer>
</Table.Header>
<Table.Cell>{user.lastName}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div
style={{
display: 'inline-block',
width: layout.spacingSmall
}}
{...props}
/>
)
export default Example

Row headers can also be stuck relative to a scrollable container.

import { layout } from '@pluralsight/ps-design-system-core'
import Avatar from '@pluralsight/ps-design-system-avatar'
import Checkbox from '@pluralsight/ps-design-system-checkbox'
import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
const Example: React.FC = props => {
const [users] = React.useState([
{ firstName: 'Lucy', lastName: 'Peck', email: '[email protected]' },
{ firstName: 'Jayden', lastName: 'Morales', email: '[email protected]' },
{ firstName: 'Milton', lastName: 'Lane', email: '[email protected]' }
])
return (
<div style={{ width: 250 }}>
<Table scrollable>
<Table.Head>
<Table.Row>
<Table.Cell />
<Table.Header role="columnheader" scope="col">
First name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Last name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Email
</Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
{users.map((user, i) => (
<Table.Row key={i}>
<Table.Header role="rowheader" scope="row" sticky>
<Checkbox />
</Table.Header>
<Table.Header role="rowheader" scope="row">
<FlexContainer>
<Avatar alt="avatar" name={user.firstName} size="xSmall" />
<HorzSpacer />
<span>{user.firstName}</span>
</FlexContainer>
</Table.Header>
<Table.Cell>{user.lastName}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div
style={{
display: 'inline-block',
width: layout.spacingSmall
}}
{...props}
/>
)
export default Example

To position the sticky header relative to the viewport, the Design System exports a renderContainer component to track positioning and update the column headers accordingly.

import { layout } from '@pluralsight/ps-design-system-core'
import Avatar from '@pluralsight/ps-design-system-avatar'
import Table, { StickyContainer } from '@pluralsight/ps-design-system-table'
import React from 'react'
const Example: React.FC = props => {
const [users] = React.useState([
{ firstName: 'Lucy', lastName: 'Peck', email: '[email protected]' },
{ firstName: 'Jayden', lastName: 'Morales', email: '[email protected]' },
{ firstName: 'Milton', lastName: 'Lane', email: '[email protected]' }
])
return (
<Table renderContainer={StickyContainer}>
<Table.Head>
<Table.Row>
<Table.Header role="columnheader" scope="col" sticky>
First name
</Table.Header>
<Table.Header role="columnheader" scope="col" sticky>
Last name
</Table.Header>
<Table.Header role="columnheader" scope="col" sticky>
Email
</Table.Header>
</Table.Row>
</Table.Head>
<Table.Body>
{users.map((user, i) => (
<Table.Row key={i}>
<Table.Header role="rowheader" scope="row">
<FlexContainer>
<Avatar alt="avatar" name={user.firstName} size="xSmall" />
<HorzSpacer />
<span>{user.firstName}</span>
</FlexContainer>
</Table.Header>
<Table.Cell>{user.lastName}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div
style={{
display: 'inline-block',
width: layout.spacingSmall
}}
{...props}
/>
)
export default Example

NOTE: Sticky headers are an advanced feature and are expected to gracefully degrade in non evergreen/modern browsers.

Usage with react-table

The Design System aims to provide the UI building block necessary for great interfaces but does not implement some of the more advanced features you might need to build an interactive table. When you need more advanced features we suggest you use a library like react-table to ease implementation.

Basic react-table usage

import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
import { Column, useTable } from 'react-table'
const Example: React.FC = () => {
const columns = React.useMemo<Column[]>(() => [
{ Header: 'First name', accessor: user => user.firstName, title: "First name" },
{ Header: 'Last name', accessor: user => user.lastName, title: 'Last name' }
], [])
const data = React.useMemo(() => [
{ firstName: 'Lucy', lastName: 'Peck' },
{ firstName: 'Jayden', lastName: 'Morales' },
{ firstName: 'Milton', lastName: 'Lane' },
{ firstName: 'Dwayne', lastName: 'Kelly' }
], [])
const table = useTable({ columns, data })
return (
<Table {...table.getTableProps()}>
<Table.Head>
{table.headerGroups.map(group => (
<Table.Row {...group.getHeaderGroupProps()}>
{group.headers.map(column => (
<Table.Header {...column.getHeaderProps({ title: column.title })}>
{column.render('Header')}
</Table.Header>
))}
</Table.Row>
))}
</Table.Head>
<Table.Body {...table.getTableBodyProps}>
{table.rows
.map(row => {
table.prepareRow(row)
return row
})
.map(row => (
<Table.Row {...row.getRowProps()}>
{row.cells.map(cell => (
<Table.Cell {...cell.getCellProps()}>
{cell.render('Cell')}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
)
}
export default Example

Sorting

import ScreenReaderOnly from '@pluralsight/ps-design-system-screenreaderonly'
import Table from '@pluralsight/ps-design-system-table'
import { useUniqueId } from '@pluralsight/ps-design-system-util'
import React from 'react'
import { Column, useTable, useSortBy } from 'react-table'
const Example: React.FC = () => {
const columns = React.useMemo<Column[]>(() => [
{ Header: 'First name', accessor: user => user.firstName, title: "First name" },
{ Header: 'Last name', accessor: user => user.lastName, title: 'Last name' }
], [])
const data = React.useMemo(() => [
{ firstName: 'Lucy', lastName: 'Peck' },
{ firstName: 'Jayden', lastName: 'Morales' },
{ firstName: 'Milton', lastName: 'Lane' },
{ firstName: 'Dwayne', lastName: 'Kelly' }
], [])
const table = useTable({ columns, data }, useSortBy)
const captionId = useUniqueId()
const title = 'Employees'
const initialCaption = `${title}: Not sorted`
const [caption, setCaption] = React.useState(initialCaption)
React.useEffect(() => {
const [activeSortRule] = table.state.sortBy
if (!activeSortRule) {
setCaption(initialCaption)
return
}
const { id: ruleId, desc } = activeSortRule
const ruleDir = desc ? 'descending' : 'ascending'
setCaption(`${title} sorted by ${ruleId}: ${ruleDir} order`)
}, [table.state.sortBy, initialCaption])
return (
<Table {...table.getTableProps()} aria-labelledby={captionId}>
<caption aria-live="polite" id={captionId}>
<ScreenReaderOnly>{caption}</ScreenReaderOnly>
</caption>
<Table.Head>
{table.headerGroups.map(group => (
<Table.Row {...group.getHeaderGroupProps()}>
{group.headers.map(column => {
const { canSort, isSorted, isSortedDesc } = column
const sort = isSorted ? (isSortedDesc ? 'desc' : 'asc') : false
const title: string = (column as any).title
const sortByProps = column.getSortByToggleProps()
const headerProps = column.getHeaderProps(sortByProps)
return (
<Table.Header
{...headerProps}
role="columnheader"
scope="col"
sort={canSort ? sort : undefined}
title={title}
>
{column.render('Header')}
</Table.Header>
)
})}
</Table.Row>
))}
</Table.Head>
<Table.Body {...table.getTableBodyProps}>
{table.rows
.map(row => {
table.prepareRow(row)
return row
})
.map(row => (
<Table.Row {...row.getRowProps()}>
{row.cells.map(cell => (
<Table.Cell {...cell.getCellProps()}>
{cell.render('Cell')}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
)
}
export default Example

Multi select

import Button from '@pluralsight/ps-design-system-button'
import Checkbox from '@pluralsight/ps-design-system-checkbox'
import { layout } from '@pluralsight/ps-design-system-core'
import { ChatIcon, MoveIcon } from '@pluralsight/ps-design-system-icon'
import ScreenReaderOnly from '@pluralsight/ps-design-system-screenreaderonly'
import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
import { Column, useTable, useRowSelect } from 'react-table'
const selectionHook = (hooks: Hooks<any>) => {
hooks.visibleColumns.push(columns => [
{
Cell: SelectionCell,
Header: SelectionHeader,
disableGroupBy: true,
id: '_selection'
},
...columns
])
}
const SelectionCell: React.FC<CellProps<any>> = props => {
const { row } = props
return <TableCheckbox {...row.getToggleRowSelectedProps()} />
}
const SelectionHeader: React.FC<HeaderProps<any>> = props => {
const { getToggleAllRowsSelectedProps } = props
const style = { width: 1 }
return <TableCheckbox {...getToggleAllRowsSelectedProps({ style })} />
}
interface TableCheckboxProps extends Omit<HTMLPropsFor<'input'>, 'ref'> {
indeterminate?: boolean
}
const TableCheckbox: React.FC<TableCheckboxProps> = props => {
const { onChange, ...rest } = props
return <Checkbox onCheck={onChange} {...rest} />
}
const Example: React.FC = () => {
const columns = React.useMemo<Column[]>(() => [
{ Header: 'First name', accessor: user => user.firstName, title: "First name" },
{ Header: 'Last name', accessor: user => user.lastName, title: 'Last name' }
], [])
const data = React.useMemo(() => [
{ firstName: 'Lucy', lastName: 'Peck' },
{ firstName: 'Jayden', lastName: 'Morales' },
{ firstName: 'Milton', lastName: 'Lane' },
{ firstName: 'Dwayne', lastName: 'Kelly' }
], [])
const table = useTable({ columns, data }, useRowSelect, selectionHook)
const actionsDisabled = React.useMemo(
() => Object.keys(table.state.selectedRowIds).length <= 0,
[table.state.selectedRowIds]
)
return (
<div>
<FlexContainer>
<Button
appearance="secondary"
disabled={actionsDisabled}
icon={<MoveIcon />}
>
Move to team
</Button>
<HorzSpacer />
<Button
appearance="secondary"
disabled={actionsDisabled}
icon={<ChatIcon />}
>
Send message
</Button>
</FlexContainer>
<br />
<Table {...table.getTableProps()}>
<Table.Head>
{table.headerGroups.map(group => (
<Table.Row {...group.getHeaderGroupProps()}>
{group.headers.map(column => {
const title: string = (column as any).title
return (
<Table.Header
{...column.getHeaderProps()}
role="columnheader"
scope="col"
style={{ width: column.id === '_selection' ? 1 : undefined }}
title={title}
>
{column.render('Header')}
</Table.Header>
)
})}
</Table.Row>
))}
</Table.Head>
<Table.Body {...table.getTableBodyProps}>
{table.rows
.map(row => {
table.prepareRow(row)
return row
})
.map(row => (
<Table.Row selected={row.isSelected} {...row.getRowProps()}>
{row.cells.map(cell => (
<Table.Cell {...cell.getCellProps()}>
{cell.render('Cell')}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
</div>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div style={{ display: 'inline-block', width: layout.spacingSmall }} {...props} />
)
export default Example

Row expand/collapse

import Button from '@pluralsight/ps-design-system-button'
import Checkbox from '@pluralsight/ps-design-system-checkbox'
import { layout } from '@pluralsight/ps-design-system-core'
import { CaretDownIcon, CaretLeftIcon, CaretRightIcon } from '@pluralsight/ps-design-system-icon'
import ScreenReaderOnly from '@pluralsight/ps-design-system-screenreaderonly'
import Table from '@pluralsight/ps-design-system-table'
import React from 'react'
import { Column, useExpanded, useTable } from 'react-table'
const expanderHook = (hooks: Hooks<any>) => {
hooks.visibleColumns.push(columns => [
{
Cell: ExpanderCell,
Header: ExpanderHeader,
disableGroupBy: true,
id: '_expander'
},
...columns
])
}
const ExpanderCell: React.FC<CellProps<any>> = props => {
const { row } = props
if (!row.canExpand) return null
const style = {
paddingLeft: `calc(${layout.spacingLarge} * ${row.depth})`
}
const icon = row.isExpanded ? <CaretDownIcon /> : <CaretRightIcon />
return (
<span {...row.getToggleRowExpandedProps({ style })}>
<Button
appearance="flat"
icon={icon}
size="xSmall"
title="Expand/Collapse additional content"
/>
</span>
)
}
const ExpanderHeader: React.FC<HeaderProps<any>> = props => {
const { getToggleAllRowsExpandedProps, isAllRowsExpanded } = props
const icon = isAllRowsExpanded ? <CaretDownIcon /> : <CaretRightIcon />
return (
<span {...getToggleAllRowsExpandedProps()}>
<Button
appearance="flat"
icon={icon}
size="xSmall"
title="Expand/Collapse additional content"
{...getToggleAllRowsExpandedProps()}
/>
</span>
)
}
const Example: React.FC = () => {
const columns = React.useMemo<Column[]>(() => [
{ Header: 'First name', accessor: user => user.firstName, title: "First name" },
{ Header: 'Last name', accessor: user => user.lastName, title: 'Last name' }
], [])
const data = React.useMemo(() => [
{
firstName: 'Lucy',
lastName: 'Peck',
subRows: [
{ firstName: 'Milton', lastName: 'Lane' },
{ firstName: 'Dwayne', lastName: 'Kelly' }
]
},
{
firstName: 'Jayden',
lastName: 'Morales',
subRows: [
{ firstName: 'Milton', lastName: 'Lane' },
{ firstName: 'Dwayne', lastName: 'Kelly' }
]
},
], [])
const table = useTable({ columns, data }, useExpanded, expanderHook)
return (
<Table {...table.getTableProps()}>
<Table.Head>
{table.headerGroups.map(group => (
<Table.Row {...group.getHeaderGroupProps()}>
{group.headers.map(column => {
const title: string = (column as any).title
return (
<Table.Header
{...column.getHeaderProps()}
role="columnheader"
scope="col"
style={{ width: column.id === '_expander' ? 1 : undefined }}
title={title}
>
{column.render('Header')}
</Table.Header>
)
})}
</Table.Row>
))}
</Table.Head>
<Table.Body {...table.getTableBodyProps}>
{table.rows
.map(row => {
table.prepareRow(row)
return row
})
.map(row => (
<Table.Row {...row.getRowProps()}>
{row.cells.map(cell => (
<Table.Cell {...cell.getCellProps()}>
{cell.render('Cell')}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
)
}
export default Example

Pagination

import Button from '@pluralsight/ps-design-system-button'
import { layout } from '@pluralsight/ps-design-system-core'
import Dropdown from '@pluralsight/ps-design-system-dropdown'
import { CaretLeftIcon, CaretRightIcon } from '@pluralsight/ps-design-system-icon'
import Table from '@pluralsight/ps-design-system-table'
import { P } from '@pluralsight/ps-design-system-text'
import React from 'react'
import { TableInstance, Column, useTable, usePagination } from 'react-table'
const Example: React.FC = () => {
const columns = React.useMemo<Column[]>(() => [
{ Header: 'First name', accessor: user => user.firstName, title: "First name" },
{ Header: 'Last name', accessor: user => user.lastName, title: 'Last name' }
], [])
const data = React.useMemo(() => [
{ firstName: 'Lucy', lastName: 'Peck' },
{ firstName: 'Jayden', lastName: 'Morales' },
{ firstName: 'Milton', lastName: 'Lane' },
{ firstName: 'Dwayne', lastName: 'Kelly' },
{ firstName: 'Don', lastName: 'Morgan' },
{ firstName: 'Camila', lastName: 'Turner' },
{ firstName: 'Gabe', lastName: 'Austin' },
{ firstName: 'Jeanne', lastName: 'Pierce' },
{ firstName: 'Scarlette', lastName: 'Obrien' },
{ firstName: 'Jimmie', lastName: 'Carpenter' }
], [])
const initialState = { pageSize: 2 }
const table = useTable({ columns, data, initialState }, usePagination)
return (
<div>
<Table {...table.getTableProps()}>
<Table.Head>
{table.headerGroups.map(group => (
<Table.Row {...group.getHeaderGroupProps()}>
{group.headers.map(column => (
<Table.Header {...column.getHeaderProps({ title: column.title })}>
{column.render('Header')}
</Table.Header>
))}
</Table.Row>
))}
</Table.Head>
<Table.Body {...table.getTableBodyProps}>
{table.page
.map(row => {
table.prepareRow(row)
return row
})
.map(row => (
<Table.Row {...row.getRowProps()}>
{row.cells.map(cell => (
<Table.Cell {...cell.getCellProps()}>
{cell.render('Cell')}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
<FlexContainer>
<Paginator table={table} />
</FlexContainer>
</div>
)
}
interface PaginatorProps {
perPageOptions?: number[]
table: TableInstance
}
const Paginator: React.FC<PaginatorProps> = props => {
const { perPageOptions = [2, 5, 10], table } = props
const { pageIndex, pageSize } = table.state
const handlePrevPage = () => table.previousPage()
const handleNextPage = () => table.nextPage()
const total = table.rows.length
const cursorStart = pageIndex * pageSize + 1
const cursorEnd = Math.min(cursorStart + pageSize - 1, total)
return (
<div style={{ display: 'flex', marginBottom: layout.spacingMedium }}>
<Button
appearance="secondary"
disabled={!table.canPreviousPage}
icon={<CaretLeftIcon />}
onClick={handlePrevPage}
title="Previous page"
/>
<HorzSpacer />
<Button
appearance="secondary"
disabled={!table.canNextPage}
icon={<CaretRightIcon />}
onClick={handleNextPage}
title="Next page"
/>
<HorzSpacer />
<P>
{cursorStart.toLocaleString()}-{cursorEnd.toLocaleString()} of{' '}
{total.toLocaleString()}
</P>
<HorzSpacer />
<Dropdown
appearance="subtle"
onChange={(_evt, value) => {
table.setPageSize(Number(value))
}}
menu={
<>
{perPageOptions.map(option => (
<Dropdown.Item key={option} value={option}>
{String(option) + ' rows'}
</Dropdown.Item>
))}
</>
}
value={pageSize}
/>
</div>
)
}
const FlexContainer: React.FC = props => (
<div style={{ display: 'flex', alignItems: 'center' }} {...props} />
)
const HorzSpacer: React.FC = props => (
<div style={{ display: 'inline-block', width: layout.spacingSmall }} {...props} />
)
export default Example

Usage with react-beautiful-dnd

Table row drag and drop can be accomplished using a library such as react-beautiful-dnd.

import { colorsTextIcon } from '@pluralsight/ps-design-system-core'
import Table from '@pluralsight/ps-design-system-table'
import { useTheme } from '@pluralsight/ps-design-system-theme'
import React from 'react'
import { DragDropContext, DraggableProvided, DraggableStateSnapshot, DropResult, Droppable, DroppableProvided, Draggable } from 'react-beautiful-dnd'
const Example: React.FC = () => {
const [data, setData] = React.useState(() => [
{ id: 'lucy.peck', firstName: 'Lucy', lastName: 'Peck' },
{ id: 'jayden-morales', firstName: 'Jayden', lastName: 'Morales' },
{ id: 'milton-lane', firstName: 'Milton', lastName: 'Lane' },
{ id: 'dwayne-kelly', firstName: 'Dwayne', lastName: 'Kelly' },
{ id: 'don-morgan', firstName: 'Don', lastName: 'Morgan' },
{ id: 'camila-turner', firstName: 'Camila', lastName: 'Turner' },
{ id: 'gabe-austin', firstName: 'Gabe', lastName: 'Austin' },
{ id: 'jeanne-pierce', firstName: 'Jeanne', lastName: 'Pierce' },
{ id: 'scarlette-obrien', firstName: 'Scarlette', lastName: 'Obrien' },
{ id: 'jimmie-carpenter', firstName: 'Jimmie', lastName: 'Carpenter' }
])
const handleDragEnd = (result: DropResult) => {
const { destination, source } = result
if (!destination) return
if (destination.index === source.index) return
const nextData = reorder(data, source.index, destination.index)
setData(nextData)
}
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Table>
<Table.Head>
<Table.Row>
<Table.Header role="columnheader" scope="col" />
<Table.Header role="columnheader" scope="col">
First name
</Table.Header>
<Table.Header role="columnheader" scope="col">
Last name
</Table.Header>
</Table.Row>
</Table.Head>
<Droppable droppableId="droppable-body">
{({ droppableProps, placeholder, innerRef }: DroppableProvided) => (
<Table.Body ref={ref => { innerRef(ref) }} {...droppableProps}>
{data.map((user, i) => (
<Draggable draggableId={user.id} key={user.id} index={i}>
{(
provided: DraggableProvided,
snapshot: DraggableStateSnapshot
) => (
<DraggableRow
key={i}
ref={provided.innerRef}
provided={provided}
snapshot={snapshot}
>
<Table.Cell>{user.firstName}</Table.Cell>
<Table.Cell>{user.lastName}</Table.Cell>
</DraggableRow>
)}
</Draggable>
))}
{placeholder}
</Table.Body>
)}
</Droppable>
</Table>
</DragDropContext>
)
}
interface DraggableRowProps extends React.ComponentProps<typeof Table.Row> {
provided: DraggableProvided
snapshot: DraggableStateSnapshot
}
const DraggableRow = React.forwardRef<HTMLTableRowElement, DraggableRowProps>(
(props, ref) => {
const { children, provided, snapshot, ...rest } = props
return (
<Table.Row ref={ref} {...provided.draggableProps} {...rest}>
<Table.Cell style={{ width: 1 }}>
<Handle {...provided.dragHandleProps} />
</Table.Cell>
{children}
</Table.Row>
)
}
)
const Handle: React.FC = props => {
const dark = useTheme() === 'dark'
const [cols, rows, gutter, size] = [2, 4, 2, 2]
const matrix = new Array(rows).fill(new Array(cols).fill(null))
const shadow = matrix.map((c, i: number) =>
c.map((_r: any, j: number) => {
const x = j * size + j * gutter
const y = i * size + i * gutter
return `${x}px ${y}px`
})
)
return (
<>
<div {...props} className="handle" />
<style jsx>{`
.handle {
color: ${dark ? colorsTextIcon.lowOnDark : colorsTextIcon.lowOnLight};
cursor: grab;
display: inline-block;
height: ${(size + gutter) * rows}px;
position: relative;
width: ${(size + gutter) * cols}px;
}
.handle:before {
background-color: currentColor;
box-shadow: ${shadow.toString()};
content: ' ';
height: ${size}px;
left: ${gutter}px;
position: absolute;
top: ${gutter}px;
width: ${size}px;
}
`}</style>
</>
)
}
const reorder = (list: unknown[], startIndex: number, endIndex: number): any[] => {
const result = Array.from(list)
const [removed] = result.splice(startIndex, 1)
result.splice(endIndex, 0, removed)
return result
}
export default Example

Accessibility

WCAG 2.1 AA Compliance

100% axe-core tests
Manual audit

Props

Table

Name
Type
Description
Default
renderContainer(props) => React.ReactNodecontainer render prop(p) => <div {...p} />
scrollablebooleanenables horizontal scrollingfalse

Table.Cell

Name
Type
Description
Default
align
left | right | center
text alignmentleft

Table.Header

Name
Type
Description
Default
align
left | right | center
text alignmentleft
role
Required
columnheader | rowheader
scope
Required
col | row
sorttrue | asc | desccolumn sorting options
stickyboolean
titlestringaccessible title. required when using sortable columns

Table.Row

Name
Type
Description
Default
expandedbooleanfalse
selectedbooleanfalse