TreeTable
TreeTable renders hierarchical data with the same feature set as DataTable — sort, filter, pagination, selection, scroll, frozen, resize, reorder, editing, export.
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
Movies | 1500kb | Folder |
Usage#
import { DataTable } from '@primereact/ui/datatable';TreeTable is DataTable with the treeMode prop set on DataTable.Root. Every feature shown in DataTable continues to work over the flattened tree — the only structural difference is that the first column wraps its cell content with DataTable.RowToggle to render the expand/collapse chevron.
<DataTable.Root data={treeNodes} dataKey="key" treeMode expandedKeys={expandedKeys} onExpandedChange={(e) => setExpandedKeys(e.value)}>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }) => (
<DataTable.Row>
<DataTable.Cell>
<DataTable.RowToggle />
{item.name}
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>Data shape#
Each node carries its own key, arbitrary data, and an optional children array:
type TreeNode = {
key: string;
data: Record<string, unknown>;
children?: TreeNode[];
};Set dataKey="key" and resolve column values against item.<field> — the component flattens data onto the row so columns can read directly.
Rows are augmented with derived fields the component consumes internally: _treeLevel, _treeHasChildren, _treePosInSet, _treeSetSize. These drive the ARIA attributes (aria-level, aria-expanded, aria-posinset, aria-setsize) automatically.
Examples#
Basic#
Displays hierarchical nodes with expand/collapse chevrons on the first column.
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
Movies | 1500kb | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Video } from '@primeicons/react/video';
import { Image as ImageIcon } from '@primeicons/react/image';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
const className = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${className} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={className} />;
if (type === 'Video') return <Video className={className} />;
return <File className={className} />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{
key: '2-0',
data: { name: 'Al Pacino', size: '1000kb', type: 'Folder' },
children: [
{ key: '2-0-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-0-1', data: { name: 'Serpico', size: '500kb', type: 'Video' } }
]
},
{
key: '2-1',
data: { name: 'Robert De Niro', size: '500kb', type: 'Folder' },
children: [
{ key: '2-1-0', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } },
{ key: '2-1-1', data: { name: 'Untouchables', size: '250kb', type: 'Video' } }
]
}
]
}
];
export default function TreeTableDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Size#
Use the size prop with small or large to adjust cell padding.
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
Movies | 1500kb | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import { ToggleButton } from '@primereact/ui/togglebutton';
import { ToggleButtonGroup } from '@primereact/ui/togglebuttongroup';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{ key: '2-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-1', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } }
]
}
];
type Size = 'small' | 'normal' | 'large';
const sizeOptions: { label: string; value: Size }[] = [
{ label: 'Small', value: 'small' },
{ label: 'Normal', value: 'normal' },
{ label: 'Large', value: 'large' }
];
export default function TreeTableSizeDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [size, setSize] = React.useState<Size>('normal');
return (
<div className="w-full">
<div style={{ marginBottom: '0.75rem' }}>
<ToggleButtonGroup
allowEmpty={false}
multiple={false}
value={size}
onValueChange={(e: { value: unknown }) => setSize(e.value as Size)}
>
{sizeOptions.map((opt) => (
<ToggleButton.Root key={opt.value} value={opt.value}>
<ToggleButton.Indicator>{opt.label}</ToggleButton.Indicator>
</ToggleButton.Root>
))}
</ToggleButtonGroup>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
size={size === 'normal' ? undefined : size}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Gridlines#
Enable the showGridlines prop to render borders between cells.
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
Movies | 1500kb | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{ key: '2-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-1', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } }
]
}
];
export default function TreeTableGridlinesDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
showGridlines
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Striped Rows#
The stripedRows prop renders rows with alternating background colors across the flattened tree.
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
barcelona.jpg | 90kb | Picture |
primeui.png | 30kb | Picture |
optimus.jpg | 30kb | Picture |
Movies | 1500kb | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{ key: '2-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-1', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } }
]
}
];
export default function TreeTableStripedRowsDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '1': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
stripedRows
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Selection#
Selection works exactly like DataTable — selectionMode controls the mode, and the DataTable.Selection render prop exposes helpers for row-level and header-level toggles. Checkbox selection propagates across the hierarchy with tri-state semantics.
Single#
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function SingleSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectedKeys, setSelectedKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="single"
selectedKeys={selectedKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Multiple#
Pair selectionMode="multiple" with metaKeySelection so Ctrl/Cmd + Click toggles rows and Shift + Click selects a range.
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function MultipleSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectedKeys, setSelectedKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="multiple"
metaKeySelection
selectedKeys={selectedKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Checkbox#
Checkbox column with propagating selection — toggling a parent selects all descendants, partial state renders as indeterminate.
Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Expenses.doc | 30kb | Document |
Resume.doc | 25kb | Document |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
Movies | 1500kb | Folder |
'use client';
import { Check } from '@primeicons/react/check';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Minus } from '@primeicons/react/minus';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/types/headless/datatable';
import type { CheckboxRootChangeEvent } from '@primereact/types/primitive/checkbox';
import type { DataTableSelectionInstance } from '@primereact/types/primitive/datatable';
import { Checkbox } from '@primereact/ui/checkbox';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const nodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
},
{
key: '2',
data: { name: 'Movies', size: '1500kb', type: 'Folder' },
children: [
{
key: '2-0',
data: { name: 'Al Pacino', size: '1000kb', type: 'Folder' },
children: [
{ key: '2-0-0', data: { name: 'Scarface', size: '500kb', type: 'Video' } },
{ key: '2-0-1', data: { name: 'Serpico', size: '500kb', type: 'Video' } }
]
},
{
key: '2-1',
data: { name: 'Robert De Niro', size: '500kb', type: 'Folder' },
children: [
{ key: '2-1-0', data: { name: 'Goodfellas', size: '250kb', type: 'Video' } },
{ key: '2-1-1', data: { name: 'Untouchables', size: '250kb', type: 'Video' } }
]
}
]
}
];
export default function TreeTableSelectionDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '0-0': true });
const [selectedKeys, setSelectedKeys] = React.useState<Record<string, boolean>>({});
return (
<div className="w-full">
<div style={{ marginBottom: '0.5rem' }}>
<strong>Selected:</strong>{' '}
{Object.keys(selectedKeys)
.filter((k) => selectedKeys[k])
.join(', ') || 'None'}
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
selectionMode="multiple"
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectedKeys={selectedKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<div className="flex items-center gap-2">
<DataTable.Selection>
{({ isAllSelected, isSomeSelected, toggleAll }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggleAll(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
<span>Name</span>
</div>
</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody key={JSON.stringify(selectedKeys)}>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<DataTable.Selection>
{({ isSelected, isPartiallySelected, toggle }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isSelected}
indeterminate={isPartiallySelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggle(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Radio#
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/types/headless/datatable';
import type { DataTableSelectionInstance } from '@primereact/types/primitive/datatable';
import type { RadioButtonRootChangeEvent } from '@primereact/types/primitive/radiobutton';
import { DataTable } from '@primereact/ui/datatable';
import { RadioButton } from '@primereact/ui/radiobutton';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function RadioSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectedKeys, setSelectedKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="single"
selectedKeys={selectedKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '3rem' }} />
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<DataTable.Selection mode="radio">
{({ isSelected, toggle }: DataTableSelectionInstance) => (
<RadioButton.Root
checked={isSelected}
onCheckedChange={(e: RadioButtonRootChangeEvent) => toggle(e.originalEvent)}
>
<RadioButton.Box>
<RadioButton.Indicator match="checked" />
</RadioButton.Box>
</RadioButton.Root>
)}
</DataTable.Selection>
</DataTable.Cell>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Keyboard#
Arrow Up/Down moves focus between rows, Arrow Right/Left expands/collapses, Space or Enter toggles the focused row, and Shift + Arrow extends a range.
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/types/headless/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function KeyboardDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectedKeys, setSelectedKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const selectedCount = Object.values(selectedKeys).filter(Boolean).length;
return (
<div className="w-full">
<div className="flex items-center justify-between gap-3 mb-3 p-3 rounded-md border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-900">
<span className="text-sm text-surface-500 dark:text-surface-400">
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">↑</kbd>{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">↓</kbd> navigate,{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">→</kbd> expand,{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">←</kbd> collapse,{' '}
<kbd className="px-1.5 py-0.5 text-xs rounded bg-surface-200 dark:bg-surface-700">Space</kbd> select
</span>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Selected</span>
<Badge severity={selectedCount ? 'info' : 'secondary'}>{selectedCount}</Badge>
</div>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="multiple"
metaKeySelection
selectedKeys={selectedKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Sort#
Sorting is evaluated per tree level — siblings are sorted against each other while the hierarchy stays intact.
Single#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableSortOrderInstance } from '@primereact/types/primitive/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { OverlayBadge } from '@primereact/ui/overlaybadge';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-2">
{children}
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
);
}
export default function SortDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<SortableHeader field="name">Name</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="size">Size</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="type">Type</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Multiple#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableSortOrderInstance } from '@primereact/types/primitive/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { OverlayBadge } from '@primereact/ui/overlaybadge';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-2">
{children}
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
);
}
export default function MultiSortDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<SortableHeader field="name">Name</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="size">Size</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="type">Type</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Presort#
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function PresortDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
defaultSortField="size"
defaultSortOrder={-1}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Sort field="name" className="inline-flex items-center gap-2">
Name
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.Sort field="size" className="inline-flex items-center gap-2">
Size
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.Sort field="type" className="inline-flex items-center gap-2">
Type
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableSortOrderInstance } from '@primereact/types/primitive/datatable';
import { Badge } from '@primereact/ui/badge';
import { DataTable } from '@primereact/ui/datatable';
import { OverlayBadge } from '@primereact/ui/overlaybadge';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function PresortMultiDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
defaultMultiSortMeta={[
{ field: 'type', order: 1 },
{ field: 'name', order: 1 }
]}
removableSort
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Sort field="name" className="inline-flex items-center gap-2">
Name
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.Sort field="size" className="inline-flex items-center gap-2">
Size
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
</DataTable.THeadCell>
<DataTable.THeadCell>
<DataTable.Sort field="type" className="inline-flex items-center gap-2">
Type
<OverlayBadge className="px-1">
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
<DataTable.SortOrder asChild>
{({ index }: DataTableSortOrderInstance) => (
<Badge shape="circle" size="small" severity="info" className="mt-1">
{index}
</Badge>
)}
</DataTable.SortOrder>
</OverlayBadge>
</DataTable.Sort>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Pagination#
Pagination operates on root-level nodes — expanded children never get paginated away.
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { EllipsisH } from '@primeicons/react';
import { AngleDoubleLeft } from '@primeicons/react/angle-double-left';
import { AngleDoubleRight } from '@primeicons/react/angle-double-right';
import { AngleLeft } from '@primeicons/react/angle-left';
import { AngleRight } from '@primeicons/react/angle-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { usePaginatorChangeEvent } from '@primereact/types/headless/paginator';
import type { DataTablePaginationInstance } from '@primereact/types/primitive/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Paginator } from '@primereact/ui/paginator';
import { Tag } from '@primereact/ui/tag';
import type { PaginatorPagesInstance } from 'primereact/paginator';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function PaginationDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
paginator
defaultRows={3}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '60%' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
<DataTable.Pagination>
{({ page, rows, totalRecords, onPageChange }: DataTablePaginationInstance) => (
<Paginator.Root
page={page + 1}
total={totalRecords}
itemsPerPage={rows}
onPageChange={(e: usePaginatorChangeEvent) => {
if (e.originalEvent) onPageChange(e.originalEvent, e.value - 1);
}}
>
<Paginator.Content>
<Paginator.First>
<AngleDoubleLeft />
</Paginator.First>
<Paginator.Prev>
<AngleLeft />
</Paginator.Prev>
<Paginator.Pages>
{({ paginator }: PaginatorPagesInstance) =>
paginator?.pages.map((p, index) =>
p.type === 'page' ? (
<Paginator.Page key={index} value={p.value} />
) : (
<Paginator.Ellipsis key={index}>
<EllipsisH />
</Paginator.Ellipsis>
)
)
}
</Paginator.Pages>
<Paginator.Next>
<AngleRight />
</Paginator.Next>
<Paginator.Last>
<AngleDoubleRight />
</Paginator.Last>
</Paginator.Content>
</Paginator.Root>
)}
</DataTable.Pagination>
</DataTable.Root>
</div>
);
}
Scroll#
scrollable + scrollHeight works the same way as in DataTable; the flattened rows scroll while the sticky header stays pinned.
Vertical#
| Name | Size | Type |
|---|---|---|
Folder 1 | — | Folder |
Document 1.1 | 50kb | Document |
Text 1.2 | 163kb | Text |
Picture 1.3 | 276kb | Picture |
Video 1.4 | 389kb | Video |
Document 1.5 | 502kb | Document |
Text 1.6 | 615kb | Text |
Folder 2 | — | Folder |
Document 2.1 | 87kb | Document |
Text 2.2 | 200kb | Text |
Picture 2.3 | 313kb | Picture |
Video 2.4 | 426kb | Video |
Document 2.5 | 539kb | Document |
Text 2.6 | 652kb | Text |
Folder 3 | — | Folder |
Folder 4 | — | Folder |
Folder 5 | — | Folder |
Folder 6 | — | Folder |
Folder 7 | — | Folder |
Folder 8 | — | Folder |
Folder 9 | — | Folder |
Folder 10 | — | Folder |
Folder 11 | — | Folder |
Folder 12 | — | Folder |
Folder 13 | — | Folder |
Folder 14 | — | Folder |
Folder 15 | — | Folder |
Folder 16 | — | Folder |
Folder 17 | — | Folder |
Folder 18 | — | Folder |
Folder 19 | — | Folder |
Folder 20 | — | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
const types = ['Document', 'Text', 'Picture', 'Video'];
const nodes: TreeNode[] = Array.from({ length: 20 }, (_, i) => ({
key: String(i),
data: { name: `Folder ${i + 1}`, size: '—', type: 'Folder' },
children: Array.from({ length: 6 }, (_, j) => {
const type = types[j % types.length];
const size = ((i * 37 + j * 113) % 1950) + 50;
return {
key: `${i}-${j}`,
data: {
name: `${type} ${i + 1}.${j + 1}`,
size: `${size}kb`,
type
}
};
})
}));
export default function TreeTableScrollDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '1': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
scrollable
scrollHeight="400px"
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Horizontal#
| Name | Size | Type | Key | Level | Has Children |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function ScrollHorizontalDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
scrollable
scrollHeight="400px"
>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ minWidth: '20rem' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '10rem' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>Type</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '14rem' }}>Key</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>Level</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>Has Children</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
<DataTable.Cell>
<code className="text-xs text-surface-500 dark:text-surface-400">{String(item.key)}</code>
</DataTable.Cell>
<DataTable.Cell>{Number(item._treeLevel ?? 0)}</DataTable.Cell>
<DataTable.Cell>{item._treeHasChildren ? 'Yes' : 'No'}</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Flex#
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function ScrollFlexDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="card flex flex-col h-[24rem]">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
scrollable
scrollHeight="flex"
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Frozen Columns#
Pin the tree column (or any column) during horizontal scroll with the frozen prop on DataTable.Column.
| Name | Size | Type | Owner | Modified | Actions |
|---|---|---|---|---|---|
Folder 1 | — | Folder | Amy Elsner | 2026-01-15 | Open · Share |
Document 1.1 | 50kb | Document | Amy Elsner | 2026-01-20 | Open · Share |
Text 1.2 | 163kb | Text | Anna Fali | 2026-02-20 | Open · Share |
Picture 1.3 | 276kb | Picture | Bernardo Dominic | 2026-03-20 | Open · Share |
Video 1.4 | 389kb | Video | Ioni Bowcher | 2026-04-20 | Open · Share |
Document 1.5 | 502kb | Document | Stephen Shaw | 2026-05-20 | Open · Share |
Folder 2 | — | Folder | Anna Fali | 2026-02-15 | Open · Share |
Document 2.1 | 87kb | Document | Anna Fali | 2026-02-20 | Open · Share |
Text 2.2 | 200kb | Text | Bernardo Dominic | 2026-03-20 | Open · Share |
Picture 2.3 | 313kb | Picture | Ioni Bowcher | 2026-04-20 | Open · Share |
Video 2.4 | 426kb | Video | Stephen Shaw | 2026-05-20 | Open · Share |
Document 2.5 | 539kb | Document | Amy Elsner | 2026-06-20 | Open · Share |
Folder 3 | — | Folder | Bernardo Dominic | 2026-03-15 | Open · Share |
Folder 4 | — | Folder | Ioni Bowcher | 2026-04-15 | Open · Share |
Folder 5 | — | Folder | Stephen Shaw | 2026-05-15 | Open · Share |
Folder 6 | — | Folder | Amy Elsner | 2026-06-15 | Open · Share |
Folder 7 | — | Folder | Anna Fali | 2026-07-15 | Open · Share |
Folder 8 | — | Folder | Bernardo Dominic | 2026-08-15 | Open · Share |
Folder 9 | — | Folder | Ioni Bowcher | 2026-09-15 | Open · Share |
Folder 10 | — | Folder | Stephen Shaw | 2026-01-15 | Open · Share |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface TreeNode {
key: string;
data: { name: string; size: string; type: string; owner: string; modified: string };
children?: TreeNode[];
}
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
const owners = ['Amy Elsner', 'Anna Fali', 'Bernardo Dominic', 'Ioni Bowcher', 'Stephen Shaw'];
const types = ['Document', 'Text', 'Picture', 'Video'];
const nodes: TreeNode[] = Array.from({ length: 10 }, (_, i) => ({
key: String(i),
data: {
name: `Folder ${i + 1}`,
size: '—',
type: 'Folder',
owner: owners[i % owners.length],
modified: `2026-0${(i % 9) + 1}-15`
},
children: Array.from({ length: 5 }, (_, j) => {
const type = types[j % types.length];
const size = ((i * 37 + j * 113) % 1950) + 50;
return {
key: `${i}-${j}`,
data: {
name: `${type} ${i + 1}.${j + 1}`,
size: `${size}kb`,
type,
owner: owners[(i + j) % owners.length],
modified: `2026-0${((i + j) % 9) + 1}-20`
}
};
})
}));
export default function TreeTableFrozenColumnsDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '1': true });
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
scrollable
scrollHeight="400px"
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell frozen style={{ minWidth: '300px' }}>
Name
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '160px' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '160px' }}>Type</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '200px' }}>Owner</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '160px' }}>Modified</DataTable.THeadCell>
<DataTable.THeadCell frozen alignFrozen="right" style={{ minWidth: '140px' }}>
Actions
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell frozen>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-semibold' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
<DataTable.Cell>{String(item.owner)}</DataTable.Cell>
<DataTable.Cell>{String(item.modified)}</DataTable.Cell>
<DataTable.Cell frozen alignFrozen="right">
<span className="text-xs text-surface-500 dark:text-surface-400">Open · Share</span>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Multiple columns can be frozen on both sides — pass alignFrozen="right" to pin to the right edge.
| Name | Size | Key | Level | Has Children | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function ScrollFrozenColumnsMultiDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
scrollable
scrollHeight="400px"
>
<DataTable.TableContainer>
<DataTable.Table>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell frozen style={{ minWidth: '20rem' }}>
Name
</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '10rem' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '14rem' }}>Key</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '10rem' }}>Level</DataTable.THeadCell>
<DataTable.THeadCell style={{ minWidth: '12rem' }}>Has Children</DataTable.THeadCell>
<DataTable.THeadCell frozen alignFrozen="right" style={{ minWidth: '10rem' }}>
Type
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell frozen>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<code className="text-xs text-surface-500 dark:text-surface-400">{String(item.key)}</code>
</DataTable.Cell>
<DataTable.Cell>{Number(item._treeLevel ?? 0)}</DataTable.Cell>
<DataTable.Cell>{item._treeHasChildren ? 'Yes' : 'No'}</DataTable.Cell>
<DataTable.Cell frozen alignFrozen="right">
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Frozen Rows#
Keep specific nodes pinned above the scrollable body by rendering them inside DataTable.FrozenTBody.
| Name | Size | Type | |
|---|---|---|---|
Folder 1 | — | Folder | |
Document 1.1 | 50kb | Document | |
Text 1.2 | 163kb | Text | |
Picture 1.3 | 276kb | Picture | |
Video 1.4 | 389kb | Video | |
Folder 2 | — | Folder | |
Folder 3 | — | Folder | |
Folder 4 | — | Folder | |
Folder 5 | — | Folder | |
Folder 6 | — | Folder | |
Folder 7 | — | Folder | |
Folder 8 | — | Folder | |
Folder 9 | — | Folder | |
Folder 10 | — | Folder |
'use client';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Lock } from '@primeicons/react/lock';
import { LockOpen } from '@primeicons/react/lock-open';
import { Video } from '@primeicons/react/video';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface TreeNode {
key: string;
data: { name: string; size: string; type: string };
children?: TreeNode[];
}
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
if (type === 'Video') return <Video className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
const types = ['Document', 'Text', 'Picture', 'Video'];
const rootFolders: TreeNode[] = Array.from({ length: 10 }, (_, i) => ({
key: String(i),
data: { name: `Folder ${i + 1}`, size: '—', type: 'Folder' },
children: Array.from({ length: 4 }, (_, j) => {
const type = types[j % types.length];
const size = ((i * 37 + j * 113) % 1950) + 50;
return {
key: `${i}-${j}`,
data: { name: `${type} ${i + 1}.${j + 1}`, size: `${size}kb`, type }
};
})
}));
function flattenBranch(node: TreeNode, level = 0): Array<TreeNode & { level: number }> {
const out: Array<TreeNode & { level: number }> = [{ ...node, level }];
if (node.children) for (const child of node.children) out.push(...flattenBranch(child, level + 1));
return out;
}
export default function TreeTableFrozenRowsDemo() {
const [lockedKeys, setLockedKeys] = React.useState<string[]>(['0']);
const toggleLock = (key: string) => {
setLockedKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : prev.length >= 1 ? [key] : [...prev, key]));
};
const lockedFolders = rootFolders.filter((f) => lockedKeys.includes(f.key));
const unlockedFolders = rootFolders.filter((f) => !lockedKeys.includes(f.key));
const renderFrozenRow = (node: TreeNode & { level: number }) => (
<DataTable.Row key={node.key}>
<DataTable.Cell>
<div className="flex items-center gap-2" style={{ paddingInlineStart: `${node.level * 1.25}rem` }}>
<TypeIcon type={node.data.type} />
<span className={node.data.type === 'Folder' ? 'font-semibold' : ''}>{node.data.name}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{node.data.size}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[node.data.type] ?? 'secondary'}>{node.data.type}</Tag>
</DataTable.Cell>
<DataTable.Cell style={{ width: '4rem', textAlign: 'center' }}>
{node.level === 0 && (
<Button variant="text" severity="secondary" size="small" iconOnly onClick={() => toggleLock(node.key)} aria-label="Unlock folder">
<LockOpen />
</Button>
)}
</DataTable.Cell>
</DataTable.Row>
);
const renderBodyRow = (item: any) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-semibold' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
<DataTable.Cell style={{ width: '4rem', textAlign: 'center' }}>
{item._treeLevel === 0 && (
<Button
variant="text"
severity="secondary"
size="small"
iconOnly
disabled={lockedKeys.length >= 1}
onClick={() => toggleLock(String(item.key))}
aria-label="Lock folder"
>
<Lock />
</Button>
)}
</DataTable.Cell>
</DataTable.Row>
);
return (
<div className="w-full">
<DataTable.Root data={unlockedFolders} dataKey="key" treeMode scrollable scrollHeight="400px">
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>Type</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '4rem' }} />
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.FrozenTBody>
{lockedFolders.flatMap((folder) => flattenBranch(folder).map((node) => renderFrozenRow(node)))}
</DataTable.FrozenTBody>
<DataTable.TBody>{({ item }: { item: any }) => renderBodyRow(item)}</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Editing#
Both editMode="cell" and editMode="row" compose with treeMode. Editing state is keyed by the row key, so collapsing or expanding other branches never drops edits in flight.
Cell#
| Name | Size | Type |
|---|---|---|
Documents | 75kb | Folder |
Work | 55kb | Folder |
Expenses.doc | 30kb | Document |
Resume.doc | 25kb | Document |
Home | 20kb | Folder |
Pictures | 150kb | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success',
Video: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const initialNodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } },
{ key: '1-2', data: { name: 'optimus.jpg', size: '30kb', type: 'Picture' } }
]
}
];
function updateNode(nodes: TreeNode[], key: string, field: string, value: unknown): TreeNode[] {
return nodes.map((node) => {
if (node.key === key) {
return { ...node, data: { ...node.data, [field]: value } };
}
if (node.children) {
return { ...node, children: updateNode(node.children, key, field, value) };
}
return node;
});
}
export default function TreeTableCellEditDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>(initialNodes);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true, '0-0': true });
const pendingValueRef = React.useRef<Record<string, string>>({});
const handleCellEditComplete = React.useCallback((e: { rowIndex: number; field: string; rowData?: Record<string, unknown> }) => {
const key = e.rowData?.key as string | undefined;
if (!key) return;
const editKey = `${key}-${e.field}`;
const newValue = pendingValueRef.current[editKey];
if (newValue !== undefined) {
setNodes((prev) => updateNode(prev, key, e.field, newValue));
delete pendingValueRef.current[editKey];
}
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
editMode="cell"
onCellEditComplete={handleCellEditComplete}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>{String(item.name)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-name`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>{String(item.size)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-size`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="type" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.type)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-type`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Cell editing composes with row selection.
| Name | Size | Type |
|---|
'use client';
import { Check } from '@primeicons/react/check';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Minus } from '@primeicons/react/minus';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableSelectionEvent } from '@primereact/types/headless/datatable';
import type { CheckboxRootChangeEvent } from '@primereact/types/primitive/checkbox';
import type { DataTableSelectionInstance } from '@primereact/types/primitive/datatable';
import { Badge } from '@primereact/ui/badge';
import { Checkbox } from '@primereact/ui/checkbox';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
function updateNode(nodes: TreeTableNode[], key: string, field: string, value: unknown): TreeTableNode[] {
return nodes.map((n) => {
if (n.key === key) return { ...n, data: { ...n.data, [field]: value } };
if (n.children) return { ...n, children: updateNode(n.children, key, field, value) };
return n;
});
}
export default function CellEditSelectionDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [selectedKeys, setSelectedKeys] = React.useState<Record<string, boolean>>({});
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const pendingValueRef = React.useRef<Record<string, string>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const handleCellEditComplete = React.useCallback((e: { rowIndex: number; field: string; rowData?: Record<string, unknown> }) => {
const key = e.rowData?.key as string | undefined;
if (!key) return;
const editKey = `${key}-${e.field}`;
const newValue = pendingValueRef.current[editKey];
if (newValue !== undefined) {
setNodes((prev) => updateNode(prev, key, e.field, newValue));
delete pendingValueRef.current[editKey];
}
}, []);
const selectedCount = Object.values(selectedKeys).filter(Boolean).length;
return (
<div className="w-full">
<div className="flex items-center gap-2 mb-3">
<span className="text-sm font-medium">Selected</span>
<Badge severity={selectedCount ? 'info' : 'secondary'}>{selectedCount}</Badge>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
selectionMode="multiple"
selectedKeys={selectedKeys}
onSelectionChange={(e: DataTableSelectionEvent) => setSelectedKeys(e.value)}
editMode="cell"
onCellEditComplete={handleCellEditComplete}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '3rem' }}>
<DataTable.Selection>
{({ isAllSelected, isSomeSelected, toggleAll }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggleAll(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '50%' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '25%' }}>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<DataTable.Selection>
{({ isSelected, isPartiallySelected, toggle }: DataTableSelectionInstance) => (
<Checkbox.Root
checked={isSelected}
indeterminate={isPartiallySelected}
onCheckedChange={(e: CheckboxRootChangeEvent) => toggle(e.originalEvent)}
>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
<Checkbox.Indicator match="indeterminate">
<Minus />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
)}
</DataTable.Selection>
</DataTable.Cell>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>{String(item.name)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-name`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-size`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Row#
| Name | Size | Type | Actions |
|---|---|---|---|
Documents | 75kb | Folder | |
Work | 55kb | Folder | |
Home | 20kb | Folder | |
Pictures | 150kb | Folder |
'use client';
import { Check } from '@primeicons/react/check';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Pencil } from '@primeicons/react/pencil';
import { Times } from '@primeicons/react/times';
import type { DataTableExpansionEvent, DataTableRowEditEvent } from '@primereact/types/headless/datatable';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Text: 'secondary',
Picture: 'success'
};
function TypeIcon({ type }: { type: string }) {
if (type === 'Folder') return <Folder className="w-4 h-4 text-yellow-500" />;
if (type === 'Picture') return <ImageIcon className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
return <File className="w-4 h-4 text-surface-500 dark:text-surface-400" />;
}
interface TreeNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
children?: TreeNode[];
}
const initialNodes: TreeNode[] = [
{
key: '0',
data: { name: 'Documents', size: '75kb', type: 'Folder' },
children: [
{
key: '0-0',
data: { name: 'Work', size: '55kb', type: 'Folder' },
children: [
{ key: '0-0-0', data: { name: 'Expenses.doc', size: '30kb', type: 'Document' } },
{ key: '0-0-1', data: { name: 'Resume.doc', size: '25kb', type: 'Document' } }
]
},
{
key: '0-1',
data: { name: 'Home', size: '20kb', type: 'Folder' },
children: [{ key: '0-1-0', data: { name: 'Invoices.txt', size: '20kb', type: 'Text' } }]
}
]
},
{
key: '1',
data: { name: 'Pictures', size: '150kb', type: 'Folder' },
children: [
{ key: '1-0', data: { name: 'barcelona.jpg', size: '90kb', type: 'Picture' } },
{ key: '1-1', data: { name: 'primeui.png', size: '30kb', type: 'Picture' } }
]
}
];
function applyNodeUpdate(nodes: TreeNode[], key: string, patch: Partial<TreeNode['data']>): TreeNode[] {
return nodes.map((node) => {
if (node.key === key) {
return { ...node, data: { ...node.data, ...patch } };
}
if (node.children) {
return { ...node, children: applyNodeUpdate(node.children, key, patch) };
}
return node;
});
}
export default function TreeTableRowEditDemo() {
const [nodes, setNodes] = React.useState<TreeNode[]>(initialNodes);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [editingKeys, setEditingKeys] = React.useState<Record<string, boolean>>({});
const draftRef = React.useRef<Record<string, Partial<TreeNode['data']>>>({});
const handleSave = React.useCallback((e: DataTableRowEditEvent) => {
const key = e.data.key as string | undefined;
if (!key) return;
const patch = draftRef.current[key];
if (patch && Object.keys(patch).length > 0) {
setNodes((prev) => applyNodeUpdate(prev, key, patch));
}
delete draftRef.current[key];
}, []);
const handleCancel = React.useCallback((e: DataTableRowEditEvent) => {
const key = e.data.key as string | undefined;
if (key) delete draftRef.current[key];
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
editMode="row"
editingKeys={editingKeys}
onEditingKeysChange={(e: any) => setEditingKeys(e.value)}
onRowEditSave={handleSave}
onRowEditCancel={handleCancel}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '50%' }}>Name</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '20%' }}>Size</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '30%' }}>Type</DataTable.THeadCell>
<DataTable.THeadCell style={{ width: '8rem' }}>Actions</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => {
const key = String(item.key);
return (
<DataTable.Row key={key}>
<DataTable.RowEditor rowKey={key} rowData={item}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>{String(item.name)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
draftRef.current[key] = { ...draftRef.current[key], name: e.target.value };
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>{String(item.size)}</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
draftRef.current[key] = { ...draftRef.current[key], size: e.target.value };
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="type" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.type)}
onChange={(e: any) => {
draftRef.current[key] = { ...draftRef.current[key], type: e.target.value };
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<div style={{ display: 'flex', gap: '0.25rem' }}>
<DataTable.RowEditorInit as={Button} variant="text" severity="secondary" size="small">
<Pencil />
</DataTable.RowEditorInit>
<DataTable.RowEditorSave as={Button} variant="text" severity="success" size="small">
<Check />
</DataTable.RowEditorSave>
<DataTable.RowEditorCancel as={Button} variant="text" severity="danger" size="small">
<Times />
</DataTable.RowEditorCancel>
</div>
</DataTable.Cell>
</DataTable.RowEditor>
</DataTable.Row>
);
}}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Resize#
Resize works in tree mode via the resizableColumns prop with columnResizeMode (fit or expand).
Fit Mode#
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function ResizeDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
resizableColumns
columnResizeMode="fit"
showGridlines
>
<DataTable.TableContainer>
<DataTable.ColumnResizeIndicator />
<DataTable.Table style={{ width: '100%', tableLayout: 'fixed' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
Name
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
Size
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
Type
<DataTable.ColumnResizer />
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Expand Mode#
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function ResizeExpandDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
resizableColumns
columnResizeMode="expand"
scrollable
showGridlines
>
<DataTable.TableContainer>
<DataTable.ColumnResizeIndicator />
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
Name
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
Size
<DataTable.ColumnResizer />
</DataTable.THeadCell>
<DataTable.THeadCell>
Type
<DataTable.ColumnResizer />
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Reorder#
Drag and drop column headers to reorder columns.
| Name | Size | Type |
|---|
'use client';
import { Bars } from '@primeicons/react/bars';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableColumnReorderEvent, DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
interface Column {
field: 'name' | 'size' | 'type';
header: string;
}
const initialColumns: Column[] = [
{ field: 'name', header: 'Name' },
{ field: 'size', header: 'Size' },
{ field: 'type', header: 'Type' }
];
function renderCell(field: Column['field'], item: any) {
if (field === 'name') {
return (
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
);
}
if (field === 'type') {
return <Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>;
}
return <span className="text-sm text-surface-500 dark:text-surface-400">{String(item[field])}</span>;
}
export default function ReorderDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [columns, setColumns] = React.useState<Column[]>(initialColumns);
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
reorderableColumns
onColumnReorder={(e: DataTableColumnReorderEvent) => {
const next = [...columns];
const [moved] = next.splice(e.dragIndex, 1);
next.splice(e.dropIndex, 0, moved);
setColumns(next);
}}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
{columns.map((col) => (
<DataTable.THeadCell key={col.field}>
<DataTable.ColumnReorderTarget className="group cursor-grab select-none">
<DataTable.ColumnReorder className="text-surface-400 transition-colors group-hover:text-surface-600">
<Bars />
</DataTable.ColumnReorder>
<span className="font-medium">{col.header}</span>
</DataTable.ColumnReorderTarget>
</DataTable.THeadCell>
))}
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody key={columns.map((c) => c.field).join(',')}>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
{columns.map((col) => (
<DataTable.Cell key={col.field}>{renderCell(col.field, item)}</DataTable.Cell>
))}
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Row Reorder#
Root-level row reordering via the drag handle on DataTable.RowReorder. Children stay attached to their parents.
Only root-level rows are reorderable; expand a node to see its children.
| Name | Size | Type |
|---|
'use client';
import { Bars } from '@primeicons/react/bars';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent, DataTableRowReorderEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function RowReorderDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 6));
}, []);
const handleReorder = React.useCallback((e: DataTableRowReorderEvent) => {
const flat = e.value as unknown as Array<{ key: string; _treeLevel?: number }>;
const fromNode = flat[e.dragIndex];
const toNode = flat[e.dropIndex];
if (!fromNode || !toNode) return;
if ((fromNode._treeLevel ?? 0) !== 0 || (toNode._treeLevel ?? 0) !== 0) return;
setNodes((prev) => {
const fromIdx = prev.findIndex((n) => n.key === fromNode.key);
const toIdx = prev.findIndex((n) => n.key === toNode.key);
if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) return prev;
const next = prev.slice();
const [moved] = next.splice(fromIdx, 1);
next.splice(toIdx, 0, moved);
return next;
});
}, []);
return (
<div className="w-full">
<p className="mb-3 text-sm text-surface-500 dark:text-surface-400">
Only root-level rows are reorderable; expand a node to see its children.
</p>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
reorderableRows
onRowReorder={handleReorder}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell style={{ width: '3rem' }} />
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
{Number(item._treeLevel ?? 0) === 0 ? (
<DataTable.RowReorder rowIndex={index} className="cursor-grab text-surface-400 hover:text-surface-600">
<Bars />
</DataTable.RowReorder>
) : null}
</DataTable.Cell>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Toggle#
Show/hide columns and drag to reorder them in a popover.
| Name | Size | Type |
|---|
'use client';
import { Bars } from '@primeicons/react/bars';
import { Check } from '@primeicons/react/check';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Cog } from '@primeicons/react/cog';
import { Refresh } from '@primeicons/react/refresh';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { useSortableList } from '@primereact/hooks/use-sortable-list';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { Button } from '@primereact/ui/button';
import { Checkbox } from '@primereact/ui/checkbox';
import { DataTable } from '@primereact/ui/datatable';
import { Popover } from '@primereact/ui/popover';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
interface ColumnDef {
field: 'name' | 'size' | 'type' | 'key' | 'level' | 'hasChildren';
header: string;
}
const initialColumns: ColumnDef[] = [
{ field: 'name', header: 'Name' },
{ field: 'size', header: 'Size' },
{ field: 'type', header: 'Type' },
{ field: 'key', header: 'Key' },
{ field: 'level', header: 'Level' },
{ field: 'hasChildren', header: 'Has Children' }
];
const defaultVisibleFields: ColumnDef['field'][] = ['name', 'size', 'type'];
function renderCell(field: ColumnDef['field'], item: any) {
if (field === 'name') {
return (
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
);
}
if (field === 'type') return <Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>;
if (field === 'size') return <span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>;
if (field === 'key') return <code className="text-xs text-surface-500 dark:text-surface-400">{String(item.key)}</code>;
if (field === 'level') return Number(item._treeLevel ?? 0);
if (field === 'hasChildren') return item._treeHasChildren ? 'Yes' : 'No';
return null;
}
const LIST_ID = 'columns';
function ColumnsPopover({
columns,
visibleFields,
onColumnsChange,
onVisibleFieldsChange,
onReset
}: {
columns: ColumnDef[];
visibleFields: string[];
onColumnsChange: (next: ColumnDef[]) => void;
onVisibleFieldsChange: (next: string[]) => void;
onReset: () => void;
}) {
const { dragState, getListHandlers, getItemHandlers } = useSortableList<typeof LIST_ID, ColumnDef>({
onReorder: (_listId, from, to) => {
const next = [...columns];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
onColumnsChange(next);
}
});
const toggleVisible = (field: string) => {
const set = new Set(visibleFields);
if (set.has(field)) set.delete(field);
else set.add(field);
onVisibleFieldsChange(Array.from(set));
};
const listHandlers = getListHandlers(LIST_ID);
return (
<Popover.Root>
<Popover.Trigger as={Button} variant="outlined" severity="secondary" size="small">
<Cog />
Columns
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={4} side="bottom" align="end">
<Popover.Popup className="w-72 p-0">
<div className="flex items-center justify-between gap-2 px-4 py-3 border-b border-surface-200 dark:border-surface-700">
<span className="text-sm font-semibold">Columns</span>
<Button variant="text" size="small" severity="secondary" onClick={onReset}>
<Refresh /> Reset
</Button>
</div>
<div className="py-2 max-h-80 overflow-auto" onDragOver={listHandlers.onDragOver} onDrop={listHandlers.onDrop}>
{columns.map((col, idx) => {
const checked = visibleFields.includes(col.field);
const itemHandlers = getItemHandlers(LIST_ID, idx, col);
const isDragging = dragState.draggedIndex === idx;
const isDragOver = dragState.dragOverIndex === idx && dragState.draggedIndex !== idx;
return (
<div
key={col.field}
{...itemHandlers}
className={[
'flex items-center gap-2 px-3 py-1.5 mx-1 rounded-md cursor-move select-none transition',
isDragging ? 'opacity-40' : '',
isDragOver
? 'bg-primary-50 dark:bg-primary-900/30 ring-1 ring-primary-400'
: 'hover:bg-surface-100 dark:hover:bg-surface-800'
].join(' ')}
>
<Bars className="w-3.5 h-3.5 text-surface-400 dark:text-surface-500" />
<label className="flex-1 flex items-center gap-2 cursor-pointer" onClick={(e) => e.stopPropagation()}>
<Checkbox.Root checked={checked} onCheckedChange={() => toggleVisible(col.field)}>
<Checkbox.Box>
<Checkbox.Indicator match="checked">
<Check />
</Checkbox.Indicator>
</Checkbox.Box>
</Checkbox.Root>
<span className="text-sm">{col.header}</span>
</label>
</div>
);
})}
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
export default function ColumnToggleDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [columns, setColumns] = React.useState<ColumnDef[]>(initialColumns);
const [visibleFields, setVisibleFields] = React.useState<string[]>(defaultVisibleFields);
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const resetOptions = () => {
setColumns(initialColumns);
setVisibleFields(defaultVisibleFields);
};
const visibleColumns = columns.filter((col) => visibleFields.includes(col.field));
return (
<div className="w-full">
<div className="mb-3 flex items-center justify-end">
<ColumnsPopover
columns={columns}
visibleFields={visibleFields}
onColumnsChange={setColumns}
onVisibleFieldsChange={setVisibleFields}
onReset={resetOptions}
/>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
{visibleColumns.map((col) => (
<DataTable.THeadCell key={col.field}>{col.header}</DataTable.THeadCell>
))}
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody key={visibleFields.join(',') + '|' + columns.map((c) => c.field).join(',')}>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
{visibleColumns.map((col) => (
<DataTable.Cell key={col.field}>{renderCell(col.field, item)}</DataTable.Cell>
))}
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Column Group#
Multi-level headers with rowSpan and colSpan, plus an aggregate TFoot.
| Product | Sale Rate | Profits | ||
|---|---|---|---|---|
| Last Year | This Year | Last Year | This Year | |
Europe | 60% | 45% | $766,537 | $659,964 |
Bamboo Watch | 51% | 40% | $54,406 | $43,342 |
Black Watch | 83% | 9% | $423,132 | $312,122 |
Blue T-Shirt | 49% | 22% | $288,999 | $304,500 |
Americas | 28% | 42% | $655,563 | $508,832 |
| Totals | $1,422,100 | $1,168,796 | ||
'use client';
import { ArrowDown } from '@primeicons/react/arrow-down';
import { ArrowUp } from '@primeicons/react/arrow-up';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface SalesNode {
key: string;
data: {
product: string;
lastYearSale: number;
thisYearSale: number;
lastYearProfit: number;
thisYearProfit: number;
};
children?: SalesNode[];
}
const nodes: SalesNode[] = [
{
key: 'europe',
data: { product: 'Europe', lastYearSale: 60, thisYearSale: 45, lastYearProfit: 766_537, thisYearProfit: 659_964 },
children: [
{ key: 'e-1', data: { product: 'Bamboo Watch', lastYearSale: 51, thisYearSale: 40, lastYearProfit: 54_406, thisYearProfit: 43_342 } },
{ key: 'e-2', data: { product: 'Black Watch', lastYearSale: 83, thisYearSale: 9, lastYearProfit: 423_132, thisYearProfit: 312_122 } },
{ key: 'e-3', data: { product: 'Blue T-Shirt', lastYearSale: 49, thisYearSale: 22, lastYearProfit: 288_999, thisYearProfit: 304_500 } }
]
},
{
key: 'americas',
data: { product: 'Americas', lastYearSale: 28, thisYearSale: 42, lastYearProfit: 655_563, thisYearProfit: 508_832 },
children: [
{ key: 'a-1', data: { product: 'Blue Band', lastYearSale: 38, thisYearSale: 5, lastYearProfit: 12_321, thisYearProfit: 8_500 } },
{ key: 'a-2', data: { product: 'Bracelet', lastYearSale: 17, thisYearSale: 79, lastYearProfit: 643_242, thisYearProfit: 500_332 } }
]
}
];
function Trend({ current, previous }: { current: number; previous: number }) {
const up = current >= previous;
return (
<div className="inline-flex items-center gap-1">
<span>{current}%</span>
{up ? <ArrowUp className="w-3 h-3 text-green-500" /> : <ArrowDown className="w-3 h-3 text-red-500" />}
</div>
);
}
export default function ColumnGroupDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ europe: true });
const totals = React.useMemo(() => {
return nodes.reduce(
(acc, n) => ({
lastYear: acc.lastYear + n.data.lastYearProfit,
thisYear: acc.thisYear + n.data.thisYearProfit
}),
{ lastYear: 0, thisYear: 0 }
);
}, []);
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
showGridlines
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell rowSpan={2}>Product</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
Sale Rate
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
Profits
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>Last Year</DataTable.THeadCell>
<DataTable.THeadCell>This Year</DataTable.THeadCell>
<DataTable.THeadCell>Last Year</DataTable.THeadCell>
<DataTable.THeadCell>This Year</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<span className={item._treeHasChildren ? 'font-medium' : ''}>{String(item.product)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>{Number(item.lastYearSale)}%</DataTable.Cell>
<DataTable.Cell>
<Trend current={Number(item.thisYearSale)} previous={Number(item.lastYearSale)} />
</DataTable.Cell>
<DataTable.Cell>${Number(item.lastYearProfit).toLocaleString()}</DataTable.Cell>
<DataTable.Cell>
<Tag severity={Number(item.thisYearProfit) >= Number(item.lastYearProfit) ? 'success' : 'danger'}>
${Number(item.thisYearProfit).toLocaleString()}
</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.TFoot>
<DataTable.TFootRow>
<DataTable.TFootCell colSpan={3} style={{ textAlign: 'right' }}>
<span className="font-semibold">Totals</span>
</DataTable.TFootCell>
<DataTable.TFootCell>
<span className="font-semibold">${totals.lastYear.toLocaleString()}</span>
</DataTable.TFootCell>
<DataTable.TFootCell>
<span className="font-semibold">${totals.thisYear.toLocaleString()}</span>
</DataTable.TFootCell>
</DataTable.TFootRow>
</DataTable.TFoot>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Sort and filter work on any leaf header cell in a grouped layout — wrap the header in DataTable.Sort and add a filter row below the leaf row.
| Sale Rate | Profits | |||
|---|---|---|---|---|
Europe | 60% | 45% | $766,537 | $659,964 |
Bamboo Watch | 51% | 40% | $54,406 | $43,342 |
Black Watch | 83% | 9% | $423,132 | $312,122 |
Blue T-Shirt | 49% | 22% | $288,999 | $304,500 |
Americas | 28% | 42% | $655,563 | $508,832 |
Blue Band | 38% | 5% | $12,321 | $8,500 |
Bracelet | 17% | 79% | $643,242 | $500,332 |
'use client';
import { ArrowDown } from '@primeicons/react/arrow-down';
import { ArrowUp } from '@primeicons/react/arrow-up';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { FilterMatchMode } from '@primereact/headless/datatable';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableFilterInstance } from '@primereact/types/primitive/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { InputText } from '@primereact/ui/inputtext';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
interface SalesNode {
key: string;
data: {
product: string;
lastYearSale: number;
thisYearSale: number;
lastYearProfit: number;
thisYearProfit: number;
};
children?: SalesNode[];
}
const nodes: SalesNode[] = [
{
key: 'europe',
data: { product: 'Europe', lastYearSale: 60, thisYearSale: 45, lastYearProfit: 766_537, thisYearProfit: 659_964 },
children: [
{ key: 'e-1', data: { product: 'Bamboo Watch', lastYearSale: 51, thisYearSale: 40, lastYearProfit: 54_406, thisYearProfit: 43_342 } },
{ key: 'e-2', data: { product: 'Black Watch', lastYearSale: 83, thisYearSale: 9, lastYearProfit: 423_132, thisYearProfit: 312_122 } },
{ key: 'e-3', data: { product: 'Blue T-Shirt', lastYearSale: 49, thisYearSale: 22, lastYearProfit: 288_999, thisYearProfit: 304_500 } }
]
},
{
key: 'americas',
data: { product: 'Americas', lastYearSale: 28, thisYearSale: 42, lastYearProfit: 655_563, thisYearProfit: 508_832 },
children: [
{ key: 'a-1', data: { product: 'Blue Band', lastYearSale: 38, thisYearSale: 5, lastYearProfit: 12_321, thisYearProfit: 8_500 } },
{ key: 'a-2', data: { product: 'Bracelet', lastYearSale: 17, thisYearSale: 79, lastYearProfit: 643_242, thisYearProfit: 500_332 } }
]
}
];
function Trend({ current, previous }: { current: number; previous: number }) {
const up = current >= previous;
return (
<div className="inline-flex items-center gap-1">
<span>{current}%</span>
{up ? <ArrowUp className="w-3 h-3 text-green-500" /> : <ArrowDown className="w-3 h-3 text-red-500" />}
</div>
);
}
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-1">
{children}
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
);
}
export default function ColumnGroupFilterSortDemo() {
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ europe: true, americas: true });
const [filters, setFilters] = React.useState<Record<string, { value: unknown; matchMode: FilterMatchMode }>>({
product: { value: null, matchMode: FilterMatchMode.Contains },
thisYearSale: { value: null, matchMode: FilterMatchMode.Gte }
});
return (
<div className="w-full">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
showGridlines
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '50rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell rowSpan={2}>
<SortableHeader field="product">Product</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
Sale Rate
</DataTable.THeadCell>
<DataTable.THeadCell colSpan={2} style={{ textAlign: 'center' }}>
Profits
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<SortableHeader field="lastYearSale">Last Year</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="thisYearSale">This Year</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="lastYearProfit">Last Year</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="thisYearProfit">This Year</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Filter field="product" display="row" dataType="text">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value)}
placeholder="Search"
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell>
<DataTable.Filter field="thisYearSale" display="row" dataType="numeric">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value ? Number(e.target.value) : null, 'gte')}
placeholder="≥"
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell />
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<span className={item._treeHasChildren ? 'font-medium' : ''}>{String(item.product)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>{Number(item.lastYearSale)}%</DataTable.Cell>
<DataTable.Cell>
<Trend current={Number(item.thisYearSale)} previous={Number(item.lastYearSale)} />
</DataTable.Cell>
<DataTable.Cell>${Number(item.lastYearProfit).toLocaleString()}</DataTable.Cell>
<DataTable.Cell>
<Tag severity={Number(item.thisYearProfit) >= Number(item.lastYearProfit) ? 'success' : 'danger'}>
${Number(item.thisYearProfit).toLocaleString()}
</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Filter#
Filtering uses the same filters + onFilter API as DataTable; treeMode honours it against the flattened tree. For the standalone hook used outside DataTable, see useTreeFilter.
Basic#
display="row" renders inline inputs directly under each column header and applies as the user types.
| Name | Size | Type |
|---|---|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Search } from '@primeicons/react/search';
import { Video } from '@primeicons/react/video';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { FilterMatchMode } from '@primereact/headless/datatable';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableFilterInstance } from '@primereact/types/primitive/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { IconField } from '@primereact/ui/iconfield';
import { InputText } from '@primereact/ui/inputtext';
import { Select, type SelectValueChangeEvent } from '@primereact/ui/select';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
const typeOptions = [
{ label: 'Any', value: '' },
{ label: 'Folder', value: 'Folder' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'Picture', value: 'Picture' },
{ label: 'Video', value: 'Video' }
];
export default function FilterBasicDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [globalFilter, setGlobalFilter] = React.useState('');
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
const [filters, setFilters] = React.useState<Record<string, { value: unknown; matchMode: FilterMatchMode }>>({
name: { value: null, matchMode: FilterMatchMode.Contains },
type: { value: null, matchMode: FilterMatchMode.Equals }
});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
return (
<div className="w-full">
<div className="mb-3 flex justify-end">
<IconField.Root className="w-full sm:max-w-xs">
<IconField.Inset>
<Search />
</IconField.Inset>
<InputText
type="search"
placeholder="Keyword search"
value={globalFilter}
onChange={(e: any) => setGlobalFilter(e.target.value)}
className="w-full"
/>
</IconField.Root>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
globalFilter={globalFilter}
globalFilterFields={['name', 'type']}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Filter field="name" display="row" dataType="text">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value)}
placeholder="Search name..."
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell>
<DataTable.Filter field="type" display="row">
{({ value, onChange }: DataTableFilterInstance) => (
<Select.Root
value={(value as string) ?? ''}
onValueChange={(e: SelectValueChangeEvent) => onChange({} as React.SyntheticEvent, e.value || null)}
options={typeOptions}
optionLabel="label"
optionValue="value"
size="small"
fluid
>
<Select.Trigger className="w-full">
<Select.Value placeholder="Any" />
<Select.Indicator>
<ChevronDown />
</Select.Indicator>
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
)}
</DataTable.Filter>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Advanced#
display="menu" swaps the inline input for a trigger icon that opens a popover with multiple constraints, match mode, operator, and Apply/Clear buttons.
Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Filter as FilterIcon } from '@primeicons/react/filter';
import { FilterSlash } from '@primeicons/react/filter-slash';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Plus } from '@primeicons/react/plus';
import { Search } from '@primeicons/react/search';
import { Trash } from '@primeicons/react/trash';
import { Video } from '@primeicons/react/video';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { FilterMatchMode, FilterOperator } from '@primereact/headless/datatable';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableFilterInstance } from '@primereact/types/primitive/datatable';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { IconField } from '@primereact/ui/iconfield';
import { InputText } from '@primereact/ui/inputtext';
import { Popover, type PopoverRootOpenChangeEvent } from '@primereact/ui/popover';
import { Select, type SelectValueChangeEvent } from '@primereact/ui/select';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
const operatorOptions = [
{ label: 'Match All (AND)', value: 'and' },
{ label: 'Match Any (OR)', value: 'or' }
];
const typeOptions = [
{ label: 'Any', value: '' },
{ label: 'Folder', value: 'Folder' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'Picture', value: 'Picture' },
{ label: 'Video', value: 'Video' }
];
function FilterSelect<T extends string>({
value,
onChange,
options,
placeholder
}: {
value: T;
onChange: (value: T) => void;
options: { label: string; value: T }[];
placeholder?: string;
}) {
return (
<Select.Root
value={value}
onValueChange={(e: SelectValueChangeEvent) => onChange(e.value as T)}
options={options}
optionLabel="label"
optionValue="value"
size="small"
style={{ width: '100%' }}
>
<Select.Trigger>
<Select.Value placeholder={placeholder} />
<Select.Indicator>
<ChevronDown />
</Select.Indicator>
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
);
}
function FilterPopover({
isActive,
open,
onOpenChange,
children
}: {
isActive: boolean;
open: boolean;
onOpenChange: (event: PopoverRootOpenChangeEvent) => void;
children: React.ReactNode;
}) {
return (
<Popover.Root open={open} onOpenChange={onOpenChange}>
<Popover.Trigger as={Button} variant="text" severity={isActive ? 'primary' : 'secondary'} size="small" iconOnly aria-label="Filter">
{isActive ? <FilterIcon /> : <FilterSlash />}
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner sideOffset={4} side="bottom" align="end">
<Popover.Popup className="w-80 p-4">{children}</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
function toggleFilterOverlay(
e: PopoverRootOpenChangeEvent,
overlayVisible: boolean,
onToggleOverlay: (event: React.SyntheticEvent) => void,
onHideOverlay: () => void
) {
if (e.value === overlayVisible) return;
if (e.value) {
const nativeEvent = e.originalEvent;
const fakeSynthetic = {
preventDefault: () => nativeEvent?.preventDefault(),
stopPropagation: () => nativeEvent?.stopPropagation(),
nativeEvent
} as unknown as React.SyntheticEvent;
onToggleOverlay(fakeSynthetic);
} else {
onHideOverlay();
}
}
const DEFAULT_FILTERS = {
name: { operator: FilterOperator.And, constraints: [{ value: null, matchMode: FilterMatchMode.Contains }] },
type: { value: null as string | null, matchMode: FilterMatchMode.Equals }
};
export default function FilterAdvancedDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [globalFilter, setGlobalFilter] = React.useState('');
const [filters, setFilters] = React.useState(DEFAULT_FILTERS);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData());
}, []);
const clearAll = () => {
setFilters(DEFAULT_FILTERS);
setGlobalFilter('');
};
return (
<div className="w-full">
<div className="mb-3 flex items-center justify-between gap-3">
<Button variant="outlined" size="small" onClick={clearAll}>
<FilterSlash /> Clear Filters
</Button>
<IconField.Root className="w-full sm:max-w-xs">
<IconField.Inset>
<Search />
</IconField.Inset>
<InputText
type="search"
placeholder="Keyword search"
value={globalFilter}
onChange={(e: any) => setGlobalFilter(e.target.value)}
className="w-full"
/>
</IconField.Root>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
globalFilter={globalFilter}
globalFilterFields={['name', 'type']}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<div className="flex items-center justify-between gap-2">
<span>Name</span>
<DataTable.Filter field="name" display="menu" dataType="text" maxConstraints={3}>
{({
constraints,
operator,
matchModeOptions,
isActive,
overlayVisible,
onToggleOverlay,
onHideOverlay,
onOperatorChange,
onConstraintChange,
onMatchModeChange,
onAddConstraint,
onRemoveConstraint,
onClear,
onApply,
canAddConstraint,
canRemoveConstraint
}: DataTableFilterInstance) => (
<FilterPopover
isActive={isActive}
open={overlayVisible}
onOpenChange={(e) => toggleFilterOverlay(e, overlayVisible, onToggleOverlay, onHideOverlay)}
>
<div className="mb-3 flex items-center justify-between gap-2">
<span className="text-sm font-semibold">Filter by Name</span>
{!canRemoveConstraint && (
<span className="text-xs text-surface-500 dark:text-surface-400">Up to 3 rules</span>
)}
</div>
<div className="mb-3">
<FilterSelect
value={operator}
onChange={(v) => onOperatorChange({} as React.SyntheticEvent, v as 'and' | 'or')}
options={operatorOptions}
/>
</div>
{constraints.map((constraint, idx) => (
<div
key={idx}
className="mb-3 p-3 rounded-md border border-surface-200 dark:border-surface-700 bg-surface-50 dark:bg-surface-900"
>
<div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs uppercase tracking-wider text-surface-500 dark:text-surface-400">
Rule {idx + 1}
</span>
{canRemoveConstraint && (
<Button
variant="text"
severity="secondary"
size="small"
iconOnly
onClick={(e: React.MouseEvent) => onRemoveConstraint(e, idx)}
aria-label="Remove rule"
>
<Trash />
</Button>
)}
</div>
<div className="mb-2">
<FilterSelect
value={constraint.matchMode as string}
onChange={(v) => onMatchModeChange({} as React.SyntheticEvent, idx, v)}
options={matchModeOptions.map((o) => ({
label: o.label,
value: o.value as string
}))}
/>
</div>
<InputText
value={(constraint.value as string) ?? ''}
onChange={(e: any) => onConstraintChange(e, idx, e.target.value)}
placeholder="Search by name..."
size="small"
fluid
/>
</div>
))}
{canAddConstraint && (
<Button variant="outlined" size="small" onClick={onAddConstraint} className="w-full mb-3">
<Plus /> Add Rule
</Button>
)}
<div className="flex gap-2 justify-end">
<Button variant="outlined" size="small" onClick={onClear}>
Clear
</Button>
<Button
size="small"
onClick={(e: React.MouseEvent) => {
onApply(e);
onHideOverlay();
}}
>
Apply
</Button>
</div>
</FilterPopover>
)}
</DataTable.Filter>
</div>
</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>
<div className="flex items-center justify-between gap-2">
<span>Type</span>
<DataTable.Filter field="type" display="menu">
{({
value,
isActive,
overlayVisible,
onToggleOverlay,
onHideOverlay,
onChange,
onClear,
onApply
}: DataTableFilterInstance) => (
<FilterPopover
isActive={isActive}
open={overlayVisible}
onOpenChange={(e) => toggleFilterOverlay(e, overlayVisible, onToggleOverlay, onHideOverlay)}
>
<div className="mb-3">
<span className="text-sm font-semibold">Filter by Type</span>
</div>
<div className="mb-3">
<FilterSelect
value={(value as string) ?? ''}
onChange={(v) => onChange({} as React.SyntheticEvent, v || null, 'equals')}
options={typeOptions}
placeholder="Any"
/>
</div>
<div className="flex gap-2 justify-end">
<Button variant="outlined" size="small" onClick={onClear}>
Clear
</Button>
<Button
size="small"
onClick={(e: React.MouseEvent) => {
onApply(e);
onHideOverlay();
}}
>
Apply
</Button>
</div>
</FilterPopover>
)}
</DataTable.Filter>
</div>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Export#
Only expanded (visible) rows are exported to CSV.
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { FileExport } from '@primeicons/react/file-export';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function ExportDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
return (
<div className="w-full">
<div className="flex items-center justify-between gap-3 mb-3">
<span className="text-sm text-surface-500 dark:text-surface-400">Only expanded (visible) rows are exported.</span>
<DataTable.Export as={Button} size="small" fileName="files" headers={{ name: 'Name', size: 'Size', type: 'Type' }}>
<FileExport />
Export CSV
</DataTable.Export>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Lazy Loading#
Fetch each branch on demand when the user expands it.
| Name | Size | Type |
|---|---|---|
Applications | 100kb | Folder |
Cloud | 20kb | Folder |
Desktop | 150kb | Folder |
Documents | 75kb | Folder |
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { Skeleton } from '@primereact/ui/skeleton';
import * as React from 'react';
interface LazyNode {
key: string;
data: {
name: string;
size: string;
type: string;
};
leaf?: boolean;
children?: LazyNode[];
}
const rootNodes: LazyNode[] = [
{ key: '0', data: { name: 'Applications', size: '100kb', type: 'Folder' }, leaf: false },
{ key: '1', data: { name: 'Cloud', size: '20kb', type: 'Folder' }, leaf: false },
{ key: '2', data: { name: 'Desktop', size: '150kb', type: 'Folder' }, leaf: false },
{ key: '3', data: { name: 'Documents', size: '75kb', type: 'Folder' }, leaf: false }
];
function fetchChildren(parentKey: string): Promise<LazyNode[]> {
return new Promise((resolve) => {
setTimeout(() => {
const count = 3;
const children: LazyNode[] = Array.from({ length: count }, (_, i) => ({
key: `${parentKey}-${i}`,
data: { name: `File ${parentKey}.${i + 1}`, size: `${(i + 1) * 10}kb`, type: 'Document' },
leaf: true
}));
resolve(children);
}, 600);
});
}
function PlaceholderRow() {
return (
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<Skeleton className="h-5 w-1/2" />
</DataTable.Cell>
</DataTable.Row>
);
}
export default function LazyDemo() {
const [nodes, setNodes] = React.useState<LazyNode[]>(rootNodes);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({});
const [loadingKeys, setLoadingKeys] = React.useState<Record<string, boolean>>({});
const loadChildren = React.useCallback(async (parentKey: string) => {
setLoadingKeys((p) => ({ ...p, [parentKey]: true }));
const children = await fetchChildren(parentKey);
setNodes((prev) => {
const update = (list: LazyNode[]): LazyNode[] =>
list.map((n) => (n.key === parentKey ? { ...n, children } : n.children ? { ...n, children: update(n.children) } : n));
return update(prev);
});
setLoadingKeys((p) => {
const { [parentKey]: _, ...rest } = p;
return rest;
});
}, []);
const onExpandedChange = React.useCallback(
(e: DataTableExpansionEvent) => {
setExpandedKeys(e.value);
const findNode = (list: LazyNode[], key: string): LazyNode | undefined => {
for (const n of list) {
if (n.key === key) return n;
if (n.children) {
const f = findNode(n.children, key);
if (f) return f;
}
}
return undefined;
};
Object.keys(e.value).forEach((key) => {
const node = findNode(nodes, key);
if (node && !node.leaf && !node.children && !loadingKeys[key]) {
loadChildren(key);
}
});
},
[nodes, loadingKeys, loadChildren]
);
return (
<div className="w-full">
<DataTable.Root data={nodes} dataKey="key" treeMode expandedKeys={expandedKeys} onExpandedChange={onExpandedChange}>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => {
const isLoading = loadingKeys[item.key] && expandedKeys[item.key];
return (
<React.Fragment key={String(item.key)}>
<DataTable.Row>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<span>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>{String(item.size)}</DataTable.Cell>
<DataTable.Cell>{String(item.type)}</DataTable.Cell>
</DataTable.Row>
{isLoading ? <PlaceholderRow /> : null}
</React.Fragment>
);
}}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Loading#
Overlay#
| Name | Size | Type |
|---|
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { Refresh } from '@primeicons/react/refresh';
import { Spinner } from '@primeicons/react/spinner';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Video } from '@primeicons/react/video';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
export default function LoadingOverlayDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [loading, setLoading] = React.useState(false);
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
}, []);
const refresh = React.useCallback(() => {
setLoading(true);
setTimeout(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 5));
setLoading(false);
}, 1500);
}, []);
return (
<div className="w-full">
<div className="mb-3 flex items-center justify-between gap-3">
<span className="text-sm text-surface-500 dark:text-surface-400">Click refresh to simulate a network fetch.</span>
<Button size="small" onClick={refresh} disabled={loading}>
<Refresh />
Refresh
</Button>
</div>
<div className="relative">
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
loading={loading}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</div>
</DataTable.Cell>
<DataTable.Cell>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
<DataTable.Loading>
<div className="flex flex-col items-center gap-2">
<Spinner className="w-10! h-10! animate-spin text-primary" />
<span className="text-sm text-surface-600 dark:text-surface-300">Loading nodes…</span>
</div>
</DataTable.Loading>
</DataTable.Root>
</div>
</div>
);
}
Skeleton#
| Name | Size | Type |
|---|---|---|
'use client';
import { DataTable } from '@primereact/ui/datatable';
import { Skeleton } from '@primereact/ui/skeleton';
import * as React from 'react';
const placeholders = Array.from({ length: 5 }, (_, i) => ({ key: String(i) }));
export default function LoadingSkeletonDemo() {
return (
<div className="w-full">
<DataTable.Root data={placeholders} dataKey="key" treeMode>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<Skeleton className="h-5 w-3/4" />
</DataTable.Cell>
<DataTable.Cell>
<Skeleton className="h-5 w-1/3" />
</DataTable.Cell>
<DataTable.Cell>
<Skeleton className="h-5 w-1/2" />
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Empty State#
Use EmptyTBody for a custom empty-state view.
| Name | Size | Type |
|---|---|---|
No folders yet Create your first folder to start building a tree. | ||
'use client';
import { Folder } from '@primeicons/react/folder';
import { Plus } from '@primeicons/react/plus';
import { Button } from '@primereact/ui/button';
import { DataTable } from '@primereact/ui/datatable';
export default function EmptyDemo() {
return (
<div className="w-full">
<DataTable.Root data={[]} dataKey="key" treeMode>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>Name</DataTable.THeadCell>
<DataTable.THeadCell>Size</DataTable.THeadCell>
<DataTable.THeadCell>Type</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item }: { item: any }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>{String(item.name)}</DataTable.Cell>
<DataTable.Cell>{String(item.size)}</DataTable.Cell>
<DataTable.Cell>{String(item.type)}</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
<DataTable.EmptyTBody>
<DataTable.Row>
<DataTable.Cell colSpan={3}>
<div className="flex flex-col items-center justify-center gap-3 py-10 text-center">
<div className="w-14 h-14 rounded-full bg-surface-100 dark:bg-surface-800 flex items-center justify-center">
<Folder className="w-7 h-7 text-surface-400 dark:text-surface-500" />
</div>
<div>
<p className="m-0 font-semibold text-surface-900 dark:text-surface-0">No folders yet</p>
<p className="mt-1 text-sm text-surface-500 dark:text-surface-400">
Create your first folder to start building a tree.
</p>
</div>
<Button size="small">
<Plus />
New Folder
</Button>
</div>
</DataTable.Cell>
</DataTable.Row>
</DataTable.EmptyTBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
Advanced#
Sort, filter (per-column + global), and cell editing composed in a single tree.
'use client';
import { ChevronDown } from '@primeicons/react/chevron-down';
import { ChevronRight } from '@primeicons/react/chevron-right';
import { File } from '@primeicons/react/file';
import { Folder } from '@primeicons/react/folder';
import { Image as ImageIcon } from '@primeicons/react/image';
import { Search } from '@primeicons/react/search';
import { SortAlt } from '@primeicons/react/sort-alt';
import { SortAmountDown } from '@primeicons/react/sort-amount-down';
import { SortAmountUpAlt } from '@primeicons/react/sort-amount-up-alt';
import { Video } from '@primeicons/react/video';
import { NodeService, type TreeTableNode } from '@/shared/services/node.service';
import { FilterMatchMode } from '@primereact/headless/datatable';
import type { DataTableExpansionEvent } from '@primereact/types/headless/datatable';
import type { DataTableFilterInstance } from '@primereact/types/primitive/datatable';
import { DataTable } from '@primereact/ui/datatable';
import { IconField } from '@primereact/ui/iconfield';
import { InputText } from '@primereact/ui/inputtext';
import { Select, type SelectValueChangeEvent } from '@primereact/ui/select';
import { Tag } from '@primereact/ui/tag';
import * as React from 'react';
const typeSeverity: Record<string, 'info' | 'warn' | 'success' | 'secondary'> = {
Folder: 'warn',
Document: 'info',
Resume: 'info',
Text: 'secondary',
Application: 'info',
Picture: 'success',
Video: 'success',
Zip: 'secondary',
PDF: 'info',
Link: 'secondary'
};
function TypeIcon({ type }: { type: string }) {
const cls = 'w-4 h-4 text-surface-500 dark:text-surface-400';
if (type === 'Folder') return <Folder className={`${cls} text-yellow-500`} />;
if (type === 'Picture') return <ImageIcon className={cls} />;
if (type === 'Video') return <Video className={cls} />;
return <File className={cls} />;
}
const typeOptions = [
{ label: 'Any', value: '' },
{ label: 'Folder', value: 'Folder' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'Picture', value: 'Picture' },
{ label: 'Video', value: 'Video' }
];
function SortableHeader({ field, children }: { field: string; children: React.ReactNode }) {
return (
<DataTable.Sort field={field} className="inline-flex items-center gap-1">
{children}
<DataTable.SortIndicator match="unsorted">
<SortAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="asc">
<SortAmountUpAlt />
</DataTable.SortIndicator>
<DataTable.SortIndicator match="desc">
<SortAmountDown />
</DataTable.SortIndicator>
</DataTable.Sort>
);
}
function updateNode(nodes: TreeTableNode[], key: string, field: string, value: unknown): TreeTableNode[] {
return nodes.map((n) => {
if (n.key === key) return { ...n, data: { ...n.data, [field]: value } };
if (n.children) return { ...n, children: updateNode(n.children, key, field, value) };
return n;
});
}
export default function AdvancedDemo() {
const [nodes, setNodes] = React.useState<TreeTableNode[]>([]);
const [globalFilter, setGlobalFilter] = React.useState('');
const [expandedKeys, setExpandedKeys] = React.useState<Record<string, boolean>>({ '0': true });
const [filters, setFilters] = React.useState<Record<string, { value: unknown; matchMode: FilterMatchMode }>>({
name: { value: null, matchMode: FilterMatchMode.Contains },
type: { value: null, matchMode: FilterMatchMode.Equals }
});
const pendingValueRef = React.useRef<Record<string, string>>({});
React.useEffect(() => {
setNodes(NodeService.getTreeTableNodesData().slice(0, 6));
}, []);
const handleCellEditComplete = React.useCallback((e: { rowIndex: number; field: string; rowData?: Record<string, unknown> }) => {
const key = e.rowData?.key as string | undefined;
if (!key) return;
const editKey = `${key}-${e.field}`;
const next = pendingValueRef.current[editKey];
if (next === undefined) return;
setNodes((prev) => updateNode(prev, key, e.field, next));
delete pendingValueRef.current[editKey];
}, []);
return (
<div className="w-full">
<div className="mb-3 flex justify-end">
<IconField.Root className="w-full sm:max-w-xs">
<IconField.Inset>
<Search />
</IconField.Inset>
<InputText
type="search"
placeholder="Keyword search"
value={globalFilter}
onChange={(e: any) => setGlobalFilter(e.target.value)}
className="w-full"
/>
</IconField.Root>
</div>
<DataTable.Root
data={nodes}
dataKey="key"
treeMode
expandedKeys={expandedKeys}
onExpandedChange={(e: DataTableExpansionEvent) => setExpandedKeys(e.value)}
removableSort
filters={filters}
onFilter={(e: { filters: Record<string, unknown> }) => setFilters(e.filters as typeof filters)}
globalFilter={globalFilter}
globalFilterFields={['name', 'type']}
editMode="cell"
onCellEditComplete={handleCellEditComplete}
>
<DataTable.TableContainer>
<DataTable.Table style={{ minWidth: '40rem' }}>
<DataTable.THead>
<DataTable.THeadRow>
<DataTable.THeadCell>
<SortableHeader field="name">Name</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="size">Size</SortableHeader>
</DataTable.THeadCell>
<DataTable.THeadCell>
<SortableHeader field="type">Type</SortableHeader>
</DataTable.THeadCell>
</DataTable.THeadRow>
<DataTable.THeadRow>
<DataTable.THeadCell>
<DataTable.Filter field="name" display="row" dataType="text">
{({ value, onChange }: DataTableFilterInstance) => (
<InputText
value={(value as string) ?? ''}
onChange={(e: any) => onChange(e, e.target.value)}
placeholder="Search"
size="small"
fluid
/>
)}
</DataTable.Filter>
</DataTable.THeadCell>
<DataTable.THeadCell />
<DataTable.THeadCell>
<DataTable.Filter field="type" display="row">
{({ value, onChange }: DataTableFilterInstance) => (
<Select.Root
value={(value as string) ?? ''}
onValueChange={(e: SelectValueChangeEvent) => onChange({} as React.SyntheticEvent, e.value || null)}
options={typeOptions}
optionLabel="label"
optionValue="value"
size="small"
fluid
>
<Select.Trigger className="w-full">
<Select.Value placeholder="Any" />
<Select.Indicator>
<ChevronDown />
</Select.Indicator>
</Select.Trigger>
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List />
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
)}
</DataTable.Filter>
</DataTable.THeadCell>
</DataTable.THeadRow>
</DataTable.THead>
<DataTable.TBody>
{({ item, index }: { item: any; index: number }) => (
<DataTable.Row key={String(item.key)}>
<DataTable.Cell>
<div className="flex items-center gap-2">
<DataTable.RowToggle>
<DataTable.RowToggleIndicator match="expanded">
<ChevronDown />
</DataTable.RowToggleIndicator>
<DataTable.RowToggleIndicator match="collapsed">
<ChevronRight />
</DataTable.RowToggleIndicator>
</DataTable.RowToggle>
<TypeIcon type={String(item.type)} />
<DataTable.CellEditor field="name" rowIndex={index} rowData={item} style={{ flex: 1 }}>
<DataTable.CellEditorDisplay>
<span className={item.type === 'Folder' ? 'font-medium' : ''}>{String(item.name)}</span>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.name)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-name`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</div>
</DataTable.Cell>
<DataTable.Cell>
<DataTable.CellEditor field="size" rowIndex={index} rowData={item}>
<DataTable.CellEditorDisplay>
<span className="text-sm text-surface-500 dark:text-surface-400">{String(item.size)}</span>
</DataTable.CellEditorDisplay>
<DataTable.CellEditorContent>
<InputText
defaultValue={String(item.size)}
onChange={(e: any) => {
pendingValueRef.current[`${item.key}-size`] = e.target.value;
}}
size="small"
fluid
/>
</DataTable.CellEditorContent>
</DataTable.CellEditor>
</DataTable.Cell>
<DataTable.Cell>
<Tag severity={typeSeverity[String(item.type)] ?? 'secondary'}>{String(item.type)}</Tag>
</DataTable.Cell>
</DataTable.Row>
)}
</DataTable.TBody>
</DataTable.Table>
</DataTable.TableContainer>
</DataTable.Root>
</div>
);
}
API#
Sub-Components#
See DataTable Primitive for the full sub-component API — TreeTable uses the same primitives.
Hooks#
See useDataTable for the headless hook API.
Accessibility#
See DataTable Primitive for WAI-ARIA compliance details and keyboard support. Tree-specific ARIA attributes (aria-level, aria-expanded, aria-posinset, aria-setsize) are wired automatically.