Use a nested columns array in your columnDefinitions to create multiple header rows. Any column with columns is treated as a grouped header, and its child columns are displayed beneath it.
import React from "react" import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnHeaders = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], }, { label: "Enrollment Data", columns: [ { accessor: "newEnrollments", label: "New Enrollments", }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", }, ], }, { label: "Performance Data", columns: [ { accessor: "attendanceRate", label: "Attendance Rate", }, { accessor: "completedClasses", label: "Completed Classes", }, { accessor: "classCompletionRate", label: "Class Completion Rate", }, { accessor: "graduatedStudents", label: "Graduated Students", }, ], }, ]; return ( <> <AdvancedTable columnDefinitions={columnDefinitions} tableData={MOCK_DATA} /> </> ) } export default AdvancedTableColumnHeaders
Multiple levels of column headers can also be rendered as seen here.
import React from "react"; import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json"; const AdvancedTableColumnHeadersMultiple = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], }, { label: "Enrollment Data", columns: [ { label: "Enrollment Stats", columns: [ { accessor: "newEnrollments", label: "New Enrollments", }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", }, ], }, ], }, { label: "Performance Data", columns: [ { label: "Completion Metrics", columns: [ { accessor: "completedClasses", label: "Completed Classes", }, { accessor: "classCompletionRate", label: "Class Completion Rate", }, ], }, { label: "Attendance", columns: [ { accessor: "attendanceRate", label: "Attendance Rate", }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", }, ], }, ], }, ]; return ( <> <AdvancedTable columnDefinitions={columnDefinitions} tableData={MOCK_DATA} /> </> ); }; export default AdvancedTableColumnHeadersMultiple;
import React from "react" import { AdvancedTable, Pill } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnHeadersCustomCell = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], }, { label: "Enrollment Data", columns: [ { accessor: "newEnrollments", label: "New Enrollments", customRenderer: (row, value) => ( <Pill text={value} variant="success" /> ), }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", customRenderer: (row, value) => ( <Pill text={value} variant="info" /> ), }, ], }, { label: "Performance Data", columns: [ { accessor: "attendanceRate", label: "Attendance Rate", }, { accessor: "completedClasses", label: "Completed Classes", customRenderer: (row, value) => ( <Pill text={value} variant="error" /> ), }, { accessor: "classCompletionRate", label: "Class Completion Rate", }, { accessor: "graduatedStudents", label: "Graduated Students", }, ], }, ]; return ( <> <AdvancedTable columnDefinitions={columnDefinitions} tableData={MOCK_DATA} /> </> ) } export default AdvancedTableColumnHeadersCustomCell
import React from "react" import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnHeadersVerticalBorder = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], }, { label: "Enrollment Data", columns: [ { accessor: "newEnrollments", label: "New Enrollments", }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", }, ], }, { label: "Performance Data", columns: [ { accessor: "attendanceRate", label: "Attendance Rate", }, { accessor: "completedClasses", label: "Completed Classes", }, { accessor: "classCompletionRate", label: "Class Completion Rate", }, { accessor: "graduatedStudents", label: "Graduated Students", }, ], }, ]; const tableProps = { verticalBorder: true, } return ( <> <AdvancedTable columnDefinitions={columnDefinitions} tableData={MOCK_DATA} tableProps={tableProps} /> </> ) } export default AdvancedTableColumnHeadersVerticalBorder
The columnVisibilityControl prop allows users to toggle the visibility of table columns dynamically.
The default can be enabled simply by passing { default:true } to the prop as shown. This will render the header with the icon enabled dropdown. The dropdown contains all columns present in the Table and any can be toggled on or off via the checkboxes.
NOTE: The first column will not be shown in the dropdown as an option since all the expansion logic/functionality lives there and it should always be visible.
import React from "react" import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnVisibility = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], id: "year" }, { accessor: "newEnrollments", label: "New Enrollments", id: "newEnrollments" }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", id: "scheduledMeetings" }, { accessor: "attendanceRate", label: "Attendance Rate", id: "attendanceRate" }, { accessor: "completedClasses", label: "Completed Classes", id: "completedClasses" }, { accessor: "classCompletionRate", label: "Class Completion Rate", id: "classCompletionRate" }, { accessor: "graduatedStudents", label: "Graduated Students", id: "graduatedStudents" }, ] return ( <div> <AdvancedTable columnDefinitions={columnDefinitions} columnVisibilityControl={{default: true}} tableData={MOCK_DATA} /> </div> ) } export default AdvancedTableColumnVisibility
The columnVisibilityControl prop also allows for greater control over the columnVisibility state. Devs can manage state themselves by passing in value and onChange as shown.
The additional onColumnVisibilityChange provides a callback with the current state as the argument if needed.
import React, { useState } from "react" import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnVisibilityWithState = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], id: "year" }, { accessor: "newEnrollments", label: "New Enrollments", id: "newEnrollments" }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", id: "scheduledMeetings" }, { accessor: "attendanceRate", label: "Attendance Rate", id: "attendanceRate" }, { accessor: "completedClasses", label: "Completed Classes", id: "completedClasses" }, { accessor: "classCompletionRate", label: "Class Completion Rate", id: "classCompletionRate" }, { accessor: "graduatedStudents", label: "Graduated Students", id: "graduatedStudents" }, ] const [columnVisibility, setColumnVisibility] = useState({ newEnrollments: false }) const columnVisibilityControl = { value: columnVisibility, onChange: setColumnVisibility, onColumnVisibilityChange: (currentState) => console.log(currentState), } return ( <div> <AdvancedTable columnDefinitions={columnDefinitions} columnVisibilityControl={columnVisibilityControl} tableData={MOCK_DATA} /> </div> ) } export default AdvancedTableColumnVisibilityWithState
By using the includeIds key/value pair as shown within the columnVisibilityControl prop, you can control which columns show up as options in the columnVisibility dropdown.
import React from "react" import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnVisibilityCustom = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", cellAccessors: ["quarter", "month", "day"], id: "year" }, { accessor: "newEnrollments", label: "New Enrollments", id: "newEnrollments" }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", id: "scheduledMeetings" }, { accessor: "attendanceRate", label: "Attendance Rate", id: "attendanceRate" }, { accessor: "completedClasses", label: "Completed Classes", id: "completedClasses" }, { accessor: "classCompletionRate", label: "Class Completion Rate", id: "classCompletionRate" }, { accessor: "graduatedStudents", label: "Graduated Students", id: "graduatedStudents" }, ] const columnVisibilityControl = { // This is the list of column ids that will be included in the column visibility control includeIds:["newEnrollments", "scheduledMeetings", "attendanceRate", "completedClasses"], } return ( <div> <AdvancedTable columnDefinitions={columnDefinitions} columnVisibilityControl={columnVisibilityControl} tableData={MOCK_DATA} /> </div> ) } export default AdvancedTableColumnVisibilityCustom
The columnVisibilityControl prop can also be used with multi-header columns as shown.
import React from "react" import { AdvancedTable } from 'playbook-ui' import MOCK_DATA from "./advanced_table_mock_data.json" const AdvancedTableColumnVisibilityMulti = (props) => { const columnDefinitions = [ { accessor: "year", label: "Year", id: "year", cellAccessors: ["quarter", "month", "day"], }, { label: "Enrollment Data", id: "enrollmentData", columns: [ { label: "Enrollment Stats", id: "enrollmentStats", columns: [ { accessor: "newEnrollments", label: "New Enrollments", id: "newEnrollments", }, { accessor: "scheduledMeetings", label: "Scheduled Meetings", id: "scheduledMeetings", }, ], }, ], }, { label: "Performance Data", id: "performanceData", columns: [ { label: "Completion Metrics", id: "completionMetrics", columns: [ { accessor: "completedClasses", label: "Completed Classes", id: "completedClasses", }, { accessor: "classCompletionRate", label: "Class Completion Rate", id: "classCompletionRate", }, ], }, { label: "Attendance", id: "attendance", columns: [ { accessor: "attendanceRate", label: "Attendance Rate", id: "attendanceRate", }, ], }, ], }, ]; return ( <div> <AdvancedTable columnDefinitions={columnDefinitions} columnVisibilityControl={{default: true}} tableData={MOCK_DATA} /> </div> ) } export default AdvancedTableColumnVisibilityMulti
This example combines patterns that often show up together in product tables:
columns in columnDefinitions.header function returning React (here: StarRating, sort icon, PbReactPopover menu). Parent groups are not sort targets; only leaf columns use enableSort: true (see Enable Sort By Column (Multi-Column)).header receives TanStack’s table. Call table.setSorting([{ id: "<leafColumnId>", desc: boolean }]) using the same id values as your leaf columns. Match direction icons to the built-in kit (arrow-up-arrow-down unsorted; arrow-up-wide-short / arrow-down-short-wide when sorted).pinnedRows with row ids and tableProps={{ sticky: true }} (see Pinned Rows).Implementation notes
header must be a child component if you need hooks (e.g. popover open state). Render it from header: ({ table }) => <YourHeader table={table} />.Tooltip — it can steal the first tap/click. Use a title on the button or copy in the doc instead.<button> as reference, closeOnClick="outside". Picking the same metric again should toggle desc; switching to another leaf column often defaults to desc: true to align with sortDescFirst.advanced_table_grouped_headers_composition_mock_data.json — twelve flat rows (2015–2026) with varied Count / Scheduled / % values so sorting is obvious; id "12" is 2026 and is pinned by default.Other building blocks on this kit: Custom Header with Multiple Headers, Sticky Header.
/* eslint-disable react/no-multi-comp, react/prop-types */ import React, { useCallback, useState } from "react" import { AdvancedTable, Flex, Icon, List, ListItem, PbReactPopover, SectionSeparator, StarRating } from 'playbook-ui' import COMPOSITION_MOCK_DATA from "./advanced_table_grouped_headers_composition_mock_data.json" const LEAF_COUNT = "newEnrollments" const LEAF_SCHEDULED = "scheduledMeetings" const ICON_UNSORTED = "arrow-up-arrow-down" const iconSorted = (desc) => (desc ? "arrow-up-wide-short" : "arrow-down-short-wide") const STAR_MENU = [ { id: LEAF_COUNT, label: "Count" }, { id: LEAF_SCHEDULED, label: "Scheduled" }, ] const MENU_BTN = { alignItems: "center", background: "transparent", border: "none", cursor: "pointer", display: "flex", font: "inherit", gap: 12, justifyContent: "space-between", padding: "8px 14px", textAlign: "left", width: "100%", } const labelStyle = (active) => ({ color: active ? "#0056cf" : "#242930", flex: 1, fontSize: 14, fontWeight: active ? 600 : 400, }) /** Hooks + popover; `header` callback cannot use hooks directly. */ const StarMetricGroupHeader = ({ table }) => { const [open, setOpen] = useState(false) const menuId = "playbook-star-metric-sort-menu" const sort0 = table.getState().sorting[0] const groupActive = sort0?.id === LEAF_COUNT || sort0?.id === LEAF_SCHEDULED const close = useCallback((shouldClose) => setOpen(!shouldClose), []) const toggle = useCallback((e) => { e.stopPropagation() setOpen((v) => !v) }, []) const applySort = useCallback( (columnId, e) => { e.stopPropagation() const cur = table.getState().sorting[0] const nextDesc = cur?.id === columnId ? !cur.desc : true table.setSorting([{ desc: nextDesc, id: columnId }]) setOpen(false) }, [table] ) return ( <PbReactPopover closeOnClick="outside" offset padding="none" placement="bottom-start" reference={ <button aria-controls={menuId} aria-expanded={open} aria-haspopup="menu" aria-label="Sort by Count or Scheduled" onClick={toggle} style={{ background: open ? "rgba(115, 134, 169, 0.14)" : "transparent", border: "none", borderRadius: 6, cursor: "pointer", font: "inherit", margin: 0, padding: "2px 4px", }} title="Open menu to sort by Count or Scheduled" type="button" > <Flex alignItems="center" gap="xs" justifyContent="center" > <StarRating backgroundType="outline" colorOption="primary" justifyContent="center" maxWidth="102px" rating={5} /> <Icon color={groupActive ? "primary" : "default"} fixedWidth icon={ groupActive ? iconSorted(Boolean(sort0?.desc)) : ICON_UNSORTED } size="md" /> </Flex> </button> } shouldClosePopover={close} show={open} zIndex={1200} > <Flex id={menuId} minWidth="220px" orientation="column" > <List borderless padding="none" > {STAR_MENU.map(({ id, label }, i) => { const active = sort0?.id === id return ( <React.Fragment key={id}> {i > 0 ? <SectionSeparator margin="none" /> : null} <ListItem padding="none"> <button onClick={(e) => applySort(id, e)} style={MENU_BTN} type="button" > <span style={labelStyle(active)}>{label}</span> <Icon color={active ? "primary" : "default"} fixedWidth icon={ active ? iconSorted(Boolean(sort0?.desc)) : ICON_UNSORTED } size="md" /> </button> </ListItem> </React.Fragment> ) })} </List> </Flex> </PbReactPopover> ) } const AdvancedTableGroupedHeadersComposition = (props) => { const [pinnedRows, setPinnedRows] = useState({ top: ["12"] }) const columnDefinitions = [ { accessor: "year", cellAccessors: ["quarter", "month", "day"], label: "Year", }, { columns: [ { columns: [ { accessor: "newEnrollments", enableSort: true, id: LEAF_COUNT, label: "Count", }, { accessor: "scheduledMeetings", enableSort: true, id: LEAF_SCHEDULED, label: "Scheduled", }, ], id: "starLeafPair", label: "Metrics", }, ], header: ({ table }) => ( <Flex justify="center"> <StarMetricGroupHeader table={table} /> </Flex> ), id: "starMetricGroup", label: "Rating group (custom header)", }, { columns: [ { accessor: "attendanceRate", label: "Attendance" }, { accessor: "classCompletionRate", enableSort: true, id: "classCompletionRate", label: "Completion %", }, ], label: "Performance", }, ] return ( <div> <AdvancedTable columnDefinitions={columnDefinitions} enableSortingRemoval maxHeight="md" pinnedRows={{ onChange: setPinnedRows, value: pinnedRows }} tableData={COMPOSITION_MOCK_DATA} tableProps={{ sticky: true }} > <AdvancedTable.Header enableSorting /> <AdvancedTable.Body /> </AdvancedTable> </div> ) } export default AdvancedTableGroupedHeadersComposition