@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
967 lines (872 loc) • 33.4 kB
Markdown
Import: `import { TreeView } from '@neo4j-ndl/react/next'`
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ariaLabel` | `string \| null` | ✅ | | The aria-label for the tree. Required for accessibility, unless using ariaLabelledby. Pass null to omit. |
| `ariaLabelledby` | `string` | | | The aria-labelledby for the tree. Pass a string of space-separated IDs of elements that label the tree. |
| `children` | `ReactNode` | | | The children of the tree. Should be TreeView.Item components or TreeView.SkeletonItem components only. |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
| `selectionMode` | `'multiple' \| 'single'` | | `single` | Selection mode for the tree. Defaults to "single". |
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `actionMenuItems` | `ReactNode` | | | ReactNode containing Menu.Item elements to render in an action menu. When provided, an action button is rendered. |
| `actionMenuProps` | `MenuProps & { as?: ElementType<any, keyof IntrinsicElements>; } & BaseProps<ElementType<any, keyof IntrinsicElements>>` | | | Props forwarded to the Menu sub-components (anchorRef, onClose). |
| `children` | `ReactNode` | | | Nested TreeView.Item or TreeView.SkeletonItem elements rendered as sub-items in a collapsible group. |
| `defaultExpanded` | `boolean` | | `false` | Default expansion for uncontrolled items |
| `hasChildren` | `boolean` | | | Whether the item has children. Decides if the item should render a chevron for expansion. Needed for lazy loading. |
| `isDisabled` | `boolean` | | | Whether the item is disabled |
| `isExpanded` | `boolean` | | | Whether the item is expanded. Makes the item controlled. |
| `isIndeterminate` | `boolean` | | | Whether the item is in an indeterminate state. Only meaningful in multi-select mode for parent nodes. |
| `isLoading` | `boolean` | | | Whether the item is loading. Applies aria-busy="true" to the item. |
| `isSelected` | `boolean` | | | Whether the item is selected |
| `leadingVisual` | `ReactNode` | | | Leading visual for the item. |
| `onExpandedChange` | `(isExpanded: boolean) => void` | | | Callback called when the item is expanded/collapsed. |
| `onSelectedChange` | `(isSelected: boolean) => void` | | | Callback called when the item is selected/deselected. |
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
| `title` | `ReactNode` | | | The label content displayed in the item row. |
| `tooltipContent` | `ReactNode` | | | Content rendered inside the tooltip. When provided, the item is wrapped in a Tooltip. |
| `tooltipProps` | `TooltipObjectProps` | | | Props forwarded to the Tooltip sub-components (root, trigger, content). |
| `trailingContent` | `ReactNode` | | | Trailing content for the item. Shown after the children but before the action menu. |
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. |
| `rows` | `number` | | `1` | The number of rows to render. |
Implements the keyboard interactions defined in the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
| Key | Description |
|-----|-------------|
| `ArrowDown` | Moves focus to the next visible tree item |
| `ArrowUp` | Moves focus to the previous visible tree item |
| `ArrowRight` | Expands a collapsed parent item. If already expanded, moves focus to the first child item |
| `ArrowLeft` | Collapses an expanded parent item. If already collapsed (or a leaf), moves focus to the parent item |
| `Home` | Moves focus to the first tree item |
| `End` | Moves focus to the last visible tree item |
| `Enter` | Toggles selection on the focused item, or toggles expansion if the item is not selectable |
| `Space` | Toggles selection on the focused item, or toggles expansion if the item is not selectable |
| `Shift + F10` | Opens the action menu on the focused item (when `actionMenu` is provided) |
The tree uses a roving tabindex strategy: only the currently focused item has `tabIndex={0}`, all other items have `tabIndex={-1}`. When the tree root receives focus it delegates to the previously focused item, or the first item if none was focused.
The TreeView component follows the [WAI-ARIA TreeView pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
- The root container has `role="tree"` and is labeled via `aria-label` or `aria-labelledby`
- When `selectionMode` is `"multiple"`, the root sets `aria-multiselectable="true"`
- Each item has `role="treeitem"` with `aria-level`, `aria-posinset`, and `aria-setsize` set automatically
- Parent items (items with children) set `aria-expanded` and `aria-owns` linking to their child `role="group"` container
- In single selection mode, selected items set `aria-selected`. In multiple selection mode, selected items set `aria-checked`, with `aria-checked="mixed"` for indeterminate parent nodes
- Disabled items set `aria-disabled="true"`
- Loading items set `aria-busy="true"`
- When `actionMenu` is provided, the treeitem sets `aria-keyshortcuts="Shift+F10"` and the action button sets `aria-haspopup="menu"`, `aria-expanded`, and `aria-label="Actions"`
- `TreeView.SkeletonItem` is `aria-hidden="true"` since it is a visual placeholder with no interactive content
- Child groups use `role="group"` and are `hidden` when the parent is collapsed
- Always provide either `ariaLabel` or `ariaLabelledby` on the root so the tree has an accessible name
- `role="tree"` only allows children with roles `treeitem` or `group`. The built-in sub-components handle this automatically, but wrapping items in custom elements without a valid role will break the tree semantics for assistive technologies
- In single selection mode (`selectionMode="single"`, the default), only one item should have `isSelected` set to `true` at a time. The component does not enforce this — it is the consumer's responsibility to manage the selection state
- In multiple selection mode, manage `isIndeterminate` on parent nodes to communicate partial selection via `aria-checked="mixed"`. Screen reader users rely on this to understand that some but not all children are selected
- When using `hasChildren` for lazy loading, set `isLoading` to apply `aria-busy="true"` and render `TreeView.SkeletonItem` inside the expanded item so users know content is being fetched
- The checkbox rendered in multiple selection mode has `tabIndex={-1}` — selection is driven from the treeitem via `Enter`/`Space`, not from the checkbox directly
- The action menu button has `tabIndex={-1}` and is only reachable via `Shift+F10`, keeping arrow-key navigation within the tree clean
- [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html) (A): All tree items are fully navigable and operable via keyboard
- [2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) (A): Roving tabindex ensures a logical focus order; `ArrowLeft` returns focus to the parent item
- [1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) (A): Tree hierarchy is expressed via `aria-level`, `aria-posinset`, `aria-setsize`, and `role="group"` nesting
- [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) (A): Roles (`tree`, `treeitem`, `group`), states (`aria-expanded`, `aria-selected`/`aria-checked`, `aria-disabled`, `aria-busy`), and accessible names (`aria-label`/`aria-labelledby`) are set automatically by the component
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Menu } from '@neo4j-ndl/react';
import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';
const Component = () => {
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [selected, setSelected] = useState<string | null>(null);
const toggle = (key: string) =>
setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
return (
<TreeView ariaLabel="Project files">
<TreeView.Item
title="src"
hasChildren
isExpanded={expanded['src']}
onExpandedChange={() => toggle('src')}
isSelected={selected === 'src'}
onSelectedChange={() => setSelected('src')}
leadingVisual={<FolderIconSolid />}
actionMenuItems={
<>
<Menu.Item
title="New File"
onClick={() => alert('New file in src')}
/>
<Menu.Item title="Rename" onClick={() => alert('Rename src')} />
<Menu.Item title="Delete" onClick={() => alert('Delete src')} />
</>
}
>
<TreeView.Item
title="index.ts"
isSelected={selected === 'index'}
onSelectedChange={() => setSelected('index')}
leadingVisual={<DocumentIconOutline />}
actionMenuItems={
<>
<Menu.Item title="Open" onClick={() => alert('Open index.ts')} />
<Menu.Item
title="Rename"
onClick={() => alert('Rename index.ts')}
/>
<Menu.Item
title="Delete"
onClick={() => alert('Delete index.ts')}
/>
</>
}
/>
<TreeView.Item
title="utils.ts"
isSelected={selected === 'utils'}
onSelectedChange={() => setSelected('utils')}
leadingVisual={<DocumentIconOutline />}
actionMenuItems={
<>
<Menu.Item title="Open" onClick={() => alert('Open utils.ts')} />
<Menu.Item
title="Rename"
onClick={() => alert('Rename utils.ts')}
/>
<Menu.Item
title="Delete"
onClick={() => alert('Delete utils.ts')}
/>
</>
}
/>
</TreeView.Item>
<TreeView.Item
title="tests"
hasChildren
isExpanded={expanded['tests']}
onExpandedChange={() => toggle('tests')}
isSelected={selected === 'tests'}
onSelectedChange={() => setSelected('tests')}
leadingVisual={<FolderIconSolid />}
actionMenuItems={
<>
<Menu.Item
title="Run Tests"
onClick={() => alert('Run all tests')}
/>
<Menu.Item
title="New Test"
onClick={() => alert('New test in tests')}
/>
<Menu.Item title="Delete" onClick={() => alert('Delete tests')} />
</>
}
>
<TreeView.Item
title="index.test.ts"
isSelected={selected === 'index-test'}
onSelectedChange={() => setSelected('index-test')}
leadingVisual={<DocumentIconOutline />}
actionMenuItems={
<>
<Menu.Item
title="Run"
onClick={() => alert('Run index.test.ts')}
/>
<Menu.Item
title="Rename"
onClick={() => alert('Rename index.test.ts')}
/>
<Menu.Item
title="Delete"
onClick={() => alert('Delete index.test.ts')}
/>
</>
}
/>
<TreeView.Item
title="utils.test.ts"
isSelected={selected === 'utils-test'}
onSelectedChange={() => setSelected('utils-test')}
leadingVisual={<DocumentIconOutline />}
actionMenuItems={
<>
<Menu.Item
title="Run"
onClick={() => alert('Run utils.test.ts')}
/>
<Menu.Item
title="Rename"
onClick={() => alert('Rename utils.test.ts')}
/>
<Menu.Item
title="Delete"
onClick={() => alert('Delete utils.test.ts')}
/>
</>
}
/>
</TreeView.Item>
<TreeView.Item
title="README.md"
isSelected={selected === 'readme'}
onSelectedChange={() => setSelected('readme')}
leadingVisual={<DocumentIconOutline />}
actionMenuItems={
<>
<Menu.Item title="Open" onClick={() => alert('Open README.md')} />
<Menu.Item
title="Rename"
onClick={() => alert('Rename README.md')}
/>
<Menu.Item
title="Delete"
onClick={() => alert('Delete README.md')}
/>
</>
}
/>
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
DocumentIconOutline,
FolderIconSolid,
PhotoIconOutline,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useCallback, useMemo, useRef, useState } from 'react';
type TreeNode = {
id: string;
label: string;
children?: TreeNode[];
};
const TREE_DATA: TreeNode[] = [
{
children: [
{
children: [
{ id: 'app-tsx', label: 'App.tsx' },
{ id: 'index-tsx', label: 'index.tsx' },
{ id: 'styles-css', label: 'styles.css' },
],
id: 'frontend',
label: 'Frontend',
},
{
children: [
{ id: 'server-ts', label: 'server.ts' },
{ id: 'routes-ts', label: 'routes.ts' },
],
id: 'backend',
label: 'Backend',
},
{ id: 'readme-md', label: 'README.md' },
],
id: 'projects',
label: 'Projects',
},
{
children: [
{
children: [
{ id: 'beach-jpg', label: 'beach.jpg' },
{ id: 'sunset-jpg', label: 'sunset.jpg' },
],
id: 'vacation',
label: 'Vacation',
},
{ id: 'profile-png', label: 'profile.png' },
],
id: 'photos',
label: 'Photos',
},
{
children: [
{ id: 'resume-pdf', label: 'resume.pdf' },
{ id: 'cover-letter-pdf', label: 'cover-letter.pdf' },
{ id: 'notes-txt', label: 'notes.txt' },
],
id: 'documents',
label: 'Documents',
},
{
children: [
{
children: [
{ id: 'chill-m3u', label: 'chill.m3u' },
{ id: 'workout-m3u', label: 'workout.m3u' },
],
id: 'playlists',
label: 'Playlists',
},
{ id: 'favorites-m3u', label: 'favorites.m3u' },
],
id: 'music',
label: 'Music',
},
{ id: 'todo-txt', label: 'TODO.txt' },
];
const LOAD_DELAY_MS = 600;
function iconForNode(node: TreeNode) {
if (node.children) {
return <FolderIconSolid />;
}
if (/\.(jpg|jpeg|png|gif|svg)$/i.test(node.label)) {
return <PhotoIconOutline />;
}
return <DocumentIconOutline />;
}
type NodeState = {
isExpanded: boolean;
isLoading: boolean;
hasLoaded: boolean;
};
const INITIAL_NODE_STATE: NodeState = {
hasLoaded: false,
isExpanded: false,
isLoading: false,
};
function useTreeState() {
const [nodeStates, setNodeStates] = useState<Record<string, NodeState>>({});
const [selectedId, setSelectedId] = useState<string | null>(null);
const timers = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
const getNodeState = useCallback(
(id: string): NodeState => nodeStates[id] ?? INITIAL_NODE_STATE,
[],
);
const handleExpand = useCallback((id: string, expanded: boolean) => {
setNodeStates((prev) => {
const current = prev[id] ?? INITIAL_NODE_STATE;
if (expanded && !current.hasLoaded) {
if (timers.current[id]) {
clearTimeout(timers.current[id]);
}
timers.current[id] = setTimeout(() => {
setNodeStates((p) => ({
...p,
[]: { hasLoaded: true, isExpanded: true, isLoading: false },
}));
delete timers.current[id];
}, LOAD_DELAY_MS);
return {
...prev,
[]: { hasLoaded: false, isExpanded: true, isLoading: true },
};
}
return { ...prev, [id]: { ...current, isExpanded: expanded } };
});
}, []);
const handleSelect = useCallback(
(id: string) => setSelectedId((prev) => (prev === id ? null : id)),
[],
);
return { getNodeState, handleExpand, handleSelect, selectedId };
}
type RenderContext = {
getNodeState: (id: string) => NodeState;
selectedId: string | null;
handleExpand: (id: string, expanded: boolean) => void;
handleSelect: (id: string) => void;
};
function renderNode(node: TreeNode, ctx: RenderContext) {
const hasChildren =
node.children !== undefined &&
node.children !== null &&
node.children.length > 0;
const state = ctx.getNodeState(node.id);
return (
<TreeView.Item
key={node.id}
title={node.label}
hasChildren={hasChildren}
isExpanded={state.isExpanded}
isLoading={state.isLoading}
isSelected={ctx.selectedId === node.id}
leadingVisual={iconForNode(node)}
onExpandedChange={(expanded: boolean) =>
ctx.handleExpand(node.id, expanded)
}
onSelectedChange={() => ctx.handleSelect(node.id)}
>
{hasChildren && state.isLoading && !state.hasLoaded && (
<TreeView.SkeletonItem rows={2} />
)}
{hasChildren &&
state.hasLoaded &&
node.children!.map((child) => renderNode(child, ctx))}
</TreeView.Item>
);
}
const Component = () => {
const { getNodeState, selectedId, handleExpand, handleSelect } =
useTreeState();
const ctx = useMemo<RenderContext>(
() => ({ getNodeState, handleExpand, handleSelect, selectedId }),
[],
);
return (
<TreeView ariaLabel="Lazy-loaded file explorer">
{TREE_DATA.map((node) => renderNode(node, ctx))}
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import {
DocumentIconOutline,
FolderIconSolid,
PhotoIconOutline,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useCallback, useState } from 'react';
type CheckedState = Record<string, boolean>;
const WORK_CHILDREN = ['report', 'presentation'] as const;
const DOC_CHILDREN = ['work', 'resume'] as const;
const IMAGE_CHILDREN = ['photo', 'screenshot'] as const;
const DISABLED_KEYS = new Set(['presentation']);
function getParentChecked(
checked: CheckedState,
childKeys: readonly string[],
): { isChecked: boolean; isIndeterminate: boolean } {
const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k));
if (enabledKeys.length === 0) {
return { isChecked: false, isIndeterminate: false };
}
const checkedCount = enabledKeys.filter((k) => checked[k]).length;
if (checkedCount === 0) {
return { isChecked: false, isIndeterminate: false };
}
if (checkedCount === enabledKeys.length) {
return { isChecked: true, isIndeterminate: false };
}
return { isChecked: false, isIndeterminate: true };
}
const Component = () => {
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [checked, setChecked] = useState<CheckedState>({});
const toggle = (key: string) =>
setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
const toggleCheck = useCallback((key: string) => {
setChecked((prev) => ({ ...prev, [key]: !prev[key] }));
}, []);
const toggleParent = useCallback(
(childKeys: readonly string[]) => {
const enabledKeys = childKeys.filter((k) => !DISABLED_KEYS.has(k));
const isAllChecked = enabledKeys.every((k) => checked[k]);
setChecked((prev) => {
const next = { ...prev };
for (const k of enabledKeys) {
next[k] = !isAllChecked;
}
return next;
});
},
[],
);
const docState = getParentChecked(checked, [
...DOC_CHILDREN,
...WORK_CHILDREN,
]);
const workState = getParentChecked(checked, WORK_CHILDREN);
const imageState = getParentChecked(checked, IMAGE_CHILDREN);
return (
<TreeView ariaLabel="File explorer" selectionMode="multiple">
<TreeView.Item
title="Documents"
hasChildren
isExpanded={expanded['documents']}
onExpandedChange={() => toggle('documents')}
isSelected={docState.isChecked}
isIndeterminate={docState.isIndeterminate}
onSelectedChange={() =>
toggleParent([...DOC_CHILDREN, ...WORK_CHILDREN])
}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Work"
hasChildren
isExpanded={expanded['work']}
onExpandedChange={() => toggle('work')}
isSelected={workState.isChecked}
isIndeterminate={workState.isIndeterminate}
onSelectedChange={() => toggleParent(WORK_CHILDREN)}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Report.pdf"
isSelected={!!checked['report']}
onSelectedChange={() => toggleCheck('report')}
leadingVisual={<DocumentIconOutline />}
/>
<TreeView.Item
title="Presentation.pptx"
isDisabled
onSelectedChange={() => null}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Resume.pdf"
isSelected={!!checked['resume']}
onSelectedChange={() => toggleCheck('resume')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Images"
hasChildren
isExpanded={expanded['images']}
onExpandedChange={() => toggle('images')}
isSelected={imageState.isChecked}
isIndeterminate={imageState.isIndeterminate}
onSelectedChange={() => toggleParent(IMAGE_CHILDREN)}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Photo.jpg"
isSelected={!!checked['photo']}
onSelectedChange={() => toggleCheck('photo')}
leadingVisual={<PhotoIconOutline />}
/>
<TreeView.Item
title="Screenshot.png"
isSelected={!!checked['screenshot']}
onSelectedChange={() => toggleCheck('screenshot')}
leadingVisual={<PhotoIconOutline />}
/>
</TreeView.Item>
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';
const Component = () => {
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [selected, setSelected] = useState<string | null>(null);
const toggle = (key: string) =>
setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
return (
<TreeView ariaLabel="File explorer">
<TreeView.Item
title="Documents"
hasChildren
isExpanded={expanded['documents']}
onExpandedChange={() => toggle('documents')}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Work"
hasChildren
isExpanded={expanded['work']}
onExpandedChange={() => toggle('work')}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Report.pdf"
isSelected={selected === 'report'}
onSelectedChange={() => setSelected('report')}
leadingVisual={<DocumentIconOutline />}
/>
<TreeView.Item
title="Presentation.pptx"
isSelected={selected === 'presentation'}
onSelectedChange={() => setSelected('presentation')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Resume.pdf"
isSelected={selected === 'resume'}
onSelectedChange={() => setSelected('resume')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Images"
hasChildren
isExpanded={expanded['images']}
onExpandedChange={() => toggle('images')}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Photo.jpg"
isSelected={selected === 'photo'}
onSelectedChange={() => setSelected('photo')}
leadingVisual={<DocumentIconOutline />}
/>
<TreeView.Item
title="Screenshot.png"
isSelected={selected === 'screenshot'}
onSelectedChange={() => setSelected('screenshot')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';
const Component = () => {
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [selected, setSelected] = useState<string | null>(null);
const toggle = (key: string) =>
setExpanded((prev) => ({ ...prev, [key]: !prev[key] }));
return (
<TreeView ariaLabel="File explorer">
<TreeView.Item
title="Documents"
hasChildren
isExpanded={expanded['documents']}
onExpandedChange={() => toggle('documents')}
isSelected={selected === 'documents'}
onSelectedChange={() => setSelected('documents')}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Work"
hasChildren
isExpanded={expanded['work']}
onExpandedChange={() => toggle('work')}
isSelected={selected === 'work'}
onSelectedChange={() => setSelected('work')}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item
title="Report.pdf"
isSelected={selected === 'report'}
onSelectedChange={() => setSelected('report')}
leadingVisual={<DocumentIconOutline />}
/>
<TreeView.Item
title="Presentation.pptx"
isDisabled
isSelected={selected === 'presentation'}
onSelectedChange={() => setSelected('presentation')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Resume.pdf"
isSelected={selected === 'resume'}
onSelectedChange={() => setSelected('resume')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Images (disabled)"
hasChildren
isExpanded={expanded['images']}
onExpandedChange={() => toggle('images')}
isSelected={selected === 'images'}
onSelectedChange={() => setSelected('images')}
leadingVisual={<FolderIconSolid />}
isDisabled
/>
<TreeView.Item
title="README.md"
isSelected={selected === 'readme'}
onSelectedChange={() => setSelected('readme')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Menu } from '@neo4j-ndl/react';
import {
AppearanceIcon,
CheckCircleIconSolid,
DatabaseSignalIcon,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';
const Component = () => {
const [selected, setSelected] = useState<string | null>(null);
return (
<TreeView ariaLabel="Databases" style={{ marginTop: '100px' }}>
<TreeView.Item
title="Production"
isSelected={selected === 'production'}
onSelectedChange={() => setSelected('production')}
leadingVisual={<DatabaseSignalIcon />}
trailingContent={
<CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
}
actionMenuItems={
<>
<Menu.Item title="Edit" />
<Menu.Item title="Delete" />
<Menu.Item title="Rename" />
</>
}
tooltipContent={
<>
<span className="n-font-semibold">Production</span>
<span className="n-flex n-items-center n-gap-token-4 n-mt-token-8">
<CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
: Endorsed
</span>
</>
}
/>
<TreeView.Item
title="Development"
isSelected={selected === 'development'}
onSelectedChange={() => setSelected('development')}
leadingVisual={<DatabaseSignalIcon />}
trailingContent={
<AppearanceIcon className="n-size-5 n-text-warning-bg-status" />
}
tooltipContent={
<>
<span className="n-font-semibold">Development</span>
<span className="n-flex n-items-center n-gap-token-4 n-mt-token-8">
<AppearanceIcon className="n-size-5 n-text-warning-bg-status" />:
In development
</span>
</>
}
/>
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { Menu } from '@neo4j-ndl/react';
import {
AppearanceIcon,
CheckCircleIconSolid,
DatabaseSignalIcon,
} from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';
const Component = () => {
const [selected, setSelected] = useState<string | null>(null);
return (
<TreeView ariaLabel="Databases">
<TreeView.Item
title="Production"
isSelected={selected === 'production'}
onSelectedChange={() => setSelected('production')}
leadingVisual={<DatabaseSignalIcon />}
trailingContent={
<CheckCircleIconSolid className="n-size-5 n-text-success-bg-status" />
}
actionMenuItems={
<>
<Menu.Item title="Edit" />
<Menu.Item title="Delete" />
<Menu.Item title="Rename" />
</>
}
/>
<TreeView.Item
title="Development"
isSelected={selected === 'development'}
onSelectedChange={() => setSelected('development')}
leadingVisual={<DatabaseSignalIcon />}
trailingContent={
<AppearanceIcon className="n-size-5 n-text-warning-bg-status" />
}
/>
</TreeView>
);
};
export default Component;
```
```tsx
import '@neo4j-ndl/base/lib/neo4j-ds-styles.css';
import { DocumentIconOutline, FolderIconSolid } from '@neo4j-ndl/react/icons';
import { TreeView } from '@neo4j-ndl/react/next';
import { useState } from 'react';
const Component = () => {
const [selected, setSelected] = useState<string | null>(null);
return (
<TreeView ariaLabel="File explorer">
<TreeView.Item
title="Documents (expanded by default)"
defaultExpanded={true}
leadingVisual={<FolderIconSolid />}
>
<TreeView.Item title="Work" leadingVisual={<FolderIconSolid />}>
<TreeView.Item
title="Report.pdf"
isSelected={selected === 'report'}
onSelectedChange={() => setSelected('report')}
leadingVisual={<DocumentIconOutline />}
/>
<TreeView.Item
title="Presentation.pptx"
isSelected={selected === 'presentation'}
onSelectedChange={() => setSelected('presentation')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Resume.pdf"
isSelected={selected === 'resume'}
onSelectedChange={() => setSelected('resume')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
<TreeView.Item
title="Images (collapsed by default)"
leadingVisual={<FolderIconSolid />}
defaultExpanded={false}
>
<TreeView.Item
title="Photo.jpg"
isSelected={selected === 'photo'}
onSelectedChange={() => setSelected('photo')}
leadingVisual={<DocumentIconOutline />}
/>
<TreeView.Item
title="Screenshot.png"
isSelected={selected === 'screenshot'}
onSelectedChange={() => setSelected('screenshot')}
leadingVisual={<DocumentIconOutline />}
/>
</TreeView.Item>
</TreeView>
);
};
export default Component;
```