A customizable and accessible tree view component for React, built with shadcn/ui
and lucide-react
. It allows users to display and select items from hierarchical data structures using checkboxes.
- Hierarchical Data Display: Renders nested tree structures.
- Node Selection: Allows single or multiple node selection via checkboxes.
- Expand/Collapse: Supports expanding and collapsing parent nodes to show/hide children.
- Indeterminate State: Checkboxes accurately reflect the selection state of descendant nodes (checked, unchecked, indeterminate).
- Two Selection Modes:
- Standard Mode: Selecting a parent automatically selects all its descendants. Unselecting a parent unselects descendants.
topLevelOnly
Mode: Selecting a node only selects that node. Selecting a child node does not affect the parent's selection status. Ensures only the highest selected node in any given branch is part of the finalvalue
.
- Accessibility: Implements ARIA attributes (
role="tree"
,role="treeitem"
,aria-selected
,aria-expanded
,aria-level
,aria-disabled
) for better screen reader support. - Customizable: Built with Tailwind CSS via
shadcn/ui
, allowing for easy style customization. - Controlled Component: Selection state is managed via
value
andonChange
props. - Duplicate ID Warning: Includes a development-mode warning if duplicate
TreeNode
IDs are detected.
-
React (v16.8+)
-
shadcn/ui
Components:Button
Checkbox
- (Implies Tailwind CSS is set up in your project)
-
lucide-react
: For icons (ChevronRight, ChevronDown). -
clsx
&tailwind-merge
: (Assumed, for thecn
utility function). Make sure you have acn
utility configured, typically like this:// lib/utils.ts import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
This component is provided as source code and assumes you have a React project set up, likely with shadcn/ui
already configured.
- Copy the Code: Copy the
TreeSelector.tsx
(or.jsx
) file provided above into your project's components directory (e.g.,src/components/TreeSelector.tsx
). - Adapt Imports: Adjust the import paths for
Button
,Checkbox
, and thecn
utility to match your project structure. For example, if yourshadcn/ui
components are insrc/components/ui
and utils insrc/lib
, the default imports might work:If your structure is different, update these paths accordingly.import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils";
- Install Dependencies: Ensure you have the necessary dependencies installed:
npm install lucide-react clsx tailwind-merge # or yarn add lucide-react clsx tailwind-merge # Ensure shadcn/ui Button & Checkbox are added: npx shadcn-ui@latest add button checkbox
Import the TreeSelector
component and provide it with the required props. You'll need to manage the selection state yourself using the value
and onChange
props.
"use client"; // Required if using in Next.js App Router
import React, { useState } from "react";
import { TreeSelector, type TreeNode } from "@/components/TreeSelector"; // Adjust import path
// Sample hierarchical data
const sampleTreeData: TreeNode[] = [
{
id: "node-1",
label: "Documents",
children: [
{ id: "node-1-1", label: "Work" },
{
id: "node-1-2",
label: "Personal",
children: [
{ id: "node-1-2-1", label: "Vacation Photos" },
{ id: "node-1-2-2", label: "Recipes" },
],
},
],
},
{
id: "node-2",
label: "Downloads",
children: [
{ id: "node-2-1", label: "Software" },
{ id: "node-2-2", label: "Torrents" },
],
},
{ id: "node-3", label: "Music" },
];
function MyFeature() {
// State to hold the IDs of selected nodes
const [selectedNodeIds, setSelectedNodeIds] = useState<string[]>([
"node-1-2-1", // Example: Initially select 'Vacation Photos'
]);
const [isTopLevelOnly, setIsTopLevelOnly] = useState(false);
const handleSelectionChange = (newSelectedIds: string[]) => {
console.log("Selected IDs:", newSelectedIds);
setSelectedNodeIds(newSelectedIds);
};
return (
<div className="p-4 space-y-4">
{/* Optional: Toggle for selection mode */}
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={isTopLevelOnly}
onChange={(e) => setIsTopLevelOnly(e.target.checked)}
/>
<span>Use topLevelOnly Mode</span>
</label>
<TreeSelector
treeData={sampleTreeData}
value={selectedNodeIds}
onChange={handleSelectionChange}
topLevelOnly={isTopLevelOnly}
className="max-w-md" // Optional: Add custom styling/sizing
/>
<div className="mt-4">
<h4 className="font-semibold">Current Selection:</h4>
{selectedNodeIds.length > 0 ? (
<ul>
{selectedNodeIds.map((id) => (
<li key={id}>
<code>{id}</code>
</li>
))}
</ul>
) : (
<p className="text-muted-foreground">No nodes selected.</p>
)}
</div>
</div>
);
}
export default MyFeature;
Prop | Type | Required | Default | Description |
---|---|---|---|---|
treeData |
TreeNode[] |
Yes | - | An array of root TreeNode objects representing the tree structure. |
value |
string[] |
Yes | - | An array of strings representing the IDs of the currently selected nodes. This makes it a controlled component. |
onChange |
(selectedIds: string[]) => void |
Yes | - | Callback function invoked when the selection changes. It receives an array of the newly selected node IDs. |
topLevelOnly |
boolean |
No | false |
If true , only the highest selected node in a branch is retained in the value . Selecting a child does not affect the parent. See below. |
className |
string |
No | undefined |
Optional CSS class name(s) to apply to the root div element of the component for custom styling. |
Each node in the treeData
array must conform to this structure:
type TreeNode = {
id: string; // Must be unique across the entire tree
label: string;
children?: TreeNode[]; // Optional array of child nodes
};
Important: Ensure that every id
within the treeData
is unique across the entire tree structure. Duplicate IDs will lead to unpredictable behavior and trigger a warning in development mode.
- Selection: Checking a node selects itself and all its descendants.
- Unselection: Unchecking a node unselects itself and all its descendants.
- Indeterminate State: A parent checkbox appears indeterminate (usually a dash or square) if some but not all of its descendants are selected.
- Disabled State: If a node's ancestor is selected, that node's checkbox becomes checked and disabled, as it's implicitly selected via its parent.
This mode is useful when you only want the user to select the most specific applicable category, preventing implicit selection of entire subtrees.
- Selection: Checking a node selects only that specific node. If its parent was previously selected, the parent might become unselected depending on how
onChange
updates thevalue
based on the component's internal logic designed to keep only top-level selections. When a node is checked, the component ensures none of its descendants remain in the selection set. - Unselection: Unchecking a node unselects only that specific node.
- Indeterminate State: The indeterminate state still reflects if any descendant is selected independently.
- Disabled State: A node's checkbox becomes disabled if an ancestor is selected, but it won't be automatically checked. Its state depends on whether it was independently selected before the ancestor was.
- Value Processing: The component actively processes the
value
array. If both a parent and its child are somehow included in thevalue
prop initially, the component will automatically remove the child ID from the effective selection set whentopLevelOnly
is true, ensuring only the parent (the "top level" selection in that branch) remains.
The component relies on Tailwind CSS classes, primarily through the shadcn/ui
components (Button
, Checkbox
) and internal layout div
s.
- Use the
className
prop on<TreeSelector>
to add styles (likewidth
,max-height
,border
, etc.) to the main container. - To customize the appearance of buttons, checkboxes, or text, you can either:
- Modify the base styles of your
shadcn/ui
components project-wide. - (Advanced) Modify the
TreeSelector.tsx
component directly to change specific classes if needed, though this makes future updates harder.
- Modify the base styles of your
- The main container has
role="tree"
. - Each node item has
role="treeitem"
. aria-level
indicates the depth of each node (starting at 1).aria-selected
indicates if the node itself is selected.aria-expanded
is present on nodes with children, indicating their expanded/collapsed state.aria-disabled
indicates if interaction with a node (specifically its checkbox) is disabled (e.g., because an ancestor is selected).- Checkboxes have
aria-label
derived from the node'slabel
for clarity.