Skip to content

A simple React tree component with multi-select checkboxes, featuring hierarchical selection logic (parent selection overrides descendants). Built using shadcn/ui.

Notifications You must be signed in to change notification settings

riad-azz/react-tree-selector

Repository files navigation

React Tree Selector Component

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.

Features

  • 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 final value.
  • 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 and onChange props.
  • Duplicate ID Warning: Includes a development-mode warning if duplicate TreeNode IDs are detected.

Dependencies

  • 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 the cn utility function). Make sure you have a cn 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));
    }

Installation / Setup

This component is provided as source code and assumes you have a React project set up, likely with shadcn/ui already configured.

  1. Copy the Code: Copy the TreeSelector.tsx (or .jsx) file provided above into your project's components directory (e.g., src/components/TreeSelector.tsx).
  2. Adapt Imports: Adjust the import paths for Button, Checkbox, and the cn utility to match your project structure. For example, if your shadcn/ui components are in src/components/ui and utils in src/lib, the default imports might work:
    import { Button } from "@/components/ui/button";
    import { Checkbox } from "@/components/ui/checkbox";
    import { cn } from "@/lib/utils";
    If your structure is different, update these paths accordingly.
  3. 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

Usage

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;

API

<TreeSelector /> Props

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.

TreeNode Type

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 Modes Explained

Standard Mode (topLevelOnly = false, Default)

  • 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.

topLevelOnly Mode (topLevelOnly = true)

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 the value 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 the value prop initially, the component will automatically remove the child ID from the effective selection set when topLevelOnly is true, ensuring only the parent (the "top level" selection in that branch) remains.

Styling

The component relies on Tailwind CSS classes, primarily through the shadcn/ui components (Button, Checkbox) and internal layout divs.

  • Use the className prop on <TreeSelector> to add styles (like width, 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.

Accessibility

  • 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's label for clarity.

About

A simple React tree component with multi-select checkboxes, featuring hierarchical selection logic (parent selection overrides descendants). Built using shadcn/ui.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published