Tree View Components

Interactive hierarchical tree structures for displaying files, folders, and nested data. Perfect for file explorers, navigation menus, and organizational charts.

Installation

1. Stimulus Controller Setup

Start by adding the following controller to your project:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["icon", "content"];
  static values = {
    animate: { type: Boolean, default: true }, // Whether to animate the tree view
  };

  connect() {
    // Initialize any folders that should start open
    this.element.querySelectorAll('[data-state="open"]').forEach((el) => {
      const button = el.previousElementSibling;
      if (button) {
        const icon = button.querySelector('[data-tree-view-target="icon"]');
        if (icon) {
          icon.classList.add("folder-open");
          icon.innerHTML = this.openFolderSvg;
        }
      }
    });

    // Initialize closed folders with hidden attribute
    this.element.querySelectorAll('[data-tree-view-target="content"][data-state="closed"]').forEach((el) => {
      el.setAttribute("hidden", "");
    });

    this.addKeyboardListeners();
  }

  disconnect() {
    this.element.removeEventListener("keydown", this.handleKeydownBound);
    // Clean up transition listeners
    this.contentTargets.forEach((content) => {
      content.removeEventListener("transitionend", content._onTransitionEnd);
    });
  }

  addKeyboardListeners() {
    this.handleKeydownBound = this.handleKeydown.bind(this);
    this.element.addEventListener("keydown", this.handleKeydownBound);
  }

  handleKeydown(event) {
    const triggers = Array.from(
      this.element.querySelectorAll('button[data-action="click->tree-view#toggle"], [data-tree-view-item]')
    );
    const currentIndex = triggers.indexOf(document.activeElement);
    if (currentIndex === -1) return;

    switch (event.key) {
      case "ArrowUp":
        event.preventDefault();
        let prevIndex = currentIndex;
        do {
          prevIndex = (prevIndex - 1 + triggers.length) % triggers.length;
        } while (prevIndex !== currentIndex && this.isElementHidden(triggers[prevIndex]));
        triggers[prevIndex].focus();
        break;
      case "ArrowDown":
        event.preventDefault();
        let nextIndex = currentIndex;
        do {
          nextIndex = (nextIndex + 1) % triggers.length;
        } while (nextIndex !== currentIndex && this.isElementHidden(triggers[nextIndex]));
        triggers[nextIndex].focus();
        break;
      case "Enter":
      case " ":
        event.preventDefault();
        triggers[currentIndex].click();
        break;
    }
  }

  isElementHidden(element) {
    let current = element;
    while (current && current !== this.element) {
      // Check if inside a closed tree content (grid-rows-[0fr])
      const content = current.closest('[data-tree-view-target="content"]');
      if (content && content.getAttribute("data-state") === "closed") {
        return true;
      }
      current = current.parentElement;
    }
    return false;
  }

  toggle(event) {
    const button = event.currentTarget;
    const contentId = button.getAttribute("aria-controls");
    // Scope the lookup to this controller's element to avoid conflicts with other tree views on the same page
    const content = this.element.querySelector(`#${contentId}`);
    const icon = button.querySelector('[data-tree-view-target="icon"]');

    const isOpen = button.getAttribute("aria-expanded") === "true";

    // Toggle aria attributes
    button.setAttribute("aria-expanded", !isOpen);

    if (isOpen) {
      // Closing: set state first, then hide after transition
      content.setAttribute("data-state", "closed");

      // Remove any existing listener
      if (content._onTransitionEnd) {
        content.removeEventListener("transitionend", content._onTransitionEnd);
      }

      // Add hidden after transition completes
      content._onTransitionEnd = (e) => {
        if (e.propertyName === "grid-template-rows" && content.getAttribute("data-state") === "closed") {
          content.setAttribute("hidden", "");
        }
        content.removeEventListener("transitionend", content._onTransitionEnd);
      };
      content.addEventListener("transitionend", content._onTransitionEnd);
    } else {
      // Opening: remove hidden first, then change state
      content.removeAttribute("hidden");
      // Force reflow to ensure hidden is removed before animation starts
      content.offsetHeight;
      content.setAttribute("data-state", "open");
    }

    // Update icons
    if (isOpen) {
      icon.classList.remove("folder-open");
      icon.innerHTML = this.closedFolderSvg;
    } else {
      icon.classList.add("folder-open");
      icon.innerHTML = this.openFolderSvg;
    }

    // Update button state
    button.setAttribute("data-state", isOpen ? "closed" : "open");
  }

  // Toggle the checkbox associated with a file item
  selectFile(event) {
    const button = event.currentTarget;
    const checkbox = button.previousElementSibling;
    if (checkbox && checkbox.type === "checkbox" && !checkbox.disabled) {
      checkbox.click();
    }
  }

  get openFolderSvg() {
    return `<g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor">
      <path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path>
      <path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path>
    </g>`;
  }

  get closedFolderSvg() {
    return `<g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor">
      <path d="M13.75,5.25c1.105,0,2,.895,2,2v5.5c0,1.105-.895,2-2,2H4.25c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626Z"></path>
    </g>`;
  }
}

Examples

Basic Tree View

A hierarchical file and folder structure with collapsible directories and smooth animations.

<div data-controller="tree-view" data-tree-view-animate-value="true" class="h-auto w-full max-w-md p-4 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">
  <!-- Root folder "src" -->
  <div class="flex flex-col gap-y-1">
    <button type="button"
            data-action="click->tree-view#toggle"
            aria-controls="tree-content-src"
            aria-expanded="true"
            data-state="open"
            class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
      <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
      <span class="font-medium">src</span>
    </button>
    <!-- Grid wrapper for smooth animation -->
    <div id="tree-content-src"
         data-state="open"
         role="region"
         class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
         data-tree-view-target="content">
      <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">
        <!-- Folder "app" -->
        <div class="flex flex-col gap-y-1">
          <button type="button"
                  data-action="click->tree-view#toggle"
                  aria-controls="tree-content-app"
                  aria-expanded="true"
                  data-state="open"
                  class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
            <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461-1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
            <span class="font-medium">app</span>
          </button>
          <!-- Grid wrapper for smooth animation -->
          <div id="tree-content-app"
               data-state="open"
               role="region"
               class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
               data-tree-view-target="content">
            <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">
              <!-- File items in app -->
              <button type="button" data-tree-view-item class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                <span>layout.tsx</span>
              </button>
              <button type="button" data-tree-view-item class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                <span>page.tsx</span>
              </button>
              <!-- Folder "components" -->
              <div class="flex flex-col gap-y-1">
                <button type="button"
                        data-action="click->tree-view#toggle"
                        aria-controls="tree-content-components"
                        aria-expanded="true"
                        data-state="open"
                        class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                  <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
                  <span class="font-medium">components</span>
                </button>
                <!-- Grid wrapper for smooth animation -->
                <div id="tree-content-components"
                     data-state="open"
                     role="region"
                     class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
                     data-tree-view-target="content">
                  <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">
                    <!-- Folder "ui" -->
                    <div class="flex flex-col gap-y-1">
                      <button type="button"
                              data-action="click->tree-view#toggle"
                              aria-controls="tree-content-ui"
                              aria-expanded="false"
                              data-state="closed"
                              class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                        <svg data-tree-view-target="icon" class="folder-closed" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M13.75,5.25c1.105,0,2,.895,2,2v5.5c0,1.105-.895,2-2,2H4.25c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626Z"></path></g></svg>
                        <span class="font-medium">ui</span>
                      </button>
                      <!-- Grid wrapper for smooth animation -->
                      <div id="tree-content-ui"
                           data-state="closed"
                           role="region"
                           class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
                           data-tree-view-target="content"
                           hidden>
                        <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">
                          <button type="button" data-tree-view-item class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                            <span>button.tsx</span>
                          </button>
                        </div>
                      </div>
                    </div>
                    <!-- Files in "components" -->
                    <button type="button" data-tree-view-item class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                      <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                      <span>header.tsx</span>
                    </button>
                    <button type="button" data-tree-view-item class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                      <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                      <span>footer.tsx</span>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- Folder "lib" -->
        <div class="flex flex-col gap-y-1">
          <button type="button"
                  data-action="click->tree-view#toggle"
                  aria-controls="tree-content-lib"
                  aria-expanded="true"
                  data-state="open"
                  class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
            <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
            <span class="font-medium">lib</span>
          </button>
          <!-- Grid wrapper for smooth animation -->
          <div id="tree-content-lib"
               data-state="open"
               role="region"
               class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
               data-tree-view-target="content">
            <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">
              <button type="button" data-tree-view-item class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 outline-hidden focus:underline">
                <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                <span>utils.ts</span>
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Tree View with Checkboxes

A file tree with checkbox selection for each item. Select folders to automatically select all nested files and subfolders. Combines the tree-view and checkbox-select-all controllers.


<div data-controller="tree-view checkbox-select-all"
     data-tree-view-animate-value="true"
     data-checkbox-select-all-toggle-key-value="x"
     class="h-auto w-full max-w-md p-4 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800">

  <!-- Header with Select All -->
  <div class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700">
    <input
      type="checkbox"
      id="tree-select-all"
      tabindex="0"
      data-checkbox-select-all-target="selectAll"
      data-action="click->checkbox-select-all#toggleAll">
    <label for="tree-select-all" class="text-sm font-semibold text-neutral-900 dark:text-neutral-100 cursor-pointer select-none">
      Select All Files
    </label>
  </div>

  <hr class="my-2 border-neutral-200 dark:border-neutral-700">

  <!-- Root folder "src" -->
  <div class="flex flex-col gap-y-1" data-checkbox-select-all-target="parent">
    <div class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700">
      <input
        type="checkbox"
        id="folder-src"
        tabindex="-1"
        data-checkbox-select-all-target="checkbox"
        data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
      <button type="button"
              data-action="click->tree-view#toggle"
              aria-controls="tree-cb-content-src"
              aria-expanded="true"
              data-state="open"
              class="flex flex-1 items-center gap-2 text-sm outline-hidden focus:underline">
        <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
        <span class="font-medium">src</span>
      </button>
    </div>
    <!-- Grid wrapper for smooth animation -->
    <div id="tree-cb-content-src"
         data-state="open"
         role="region"
         class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
         data-tree-view-target="content">
      <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">

        <!-- Folder "app" -->
        <div class="flex flex-col gap-y-1" data-checkbox-select-all-target="parent">
          <div class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700">
            <input
              type="checkbox"
              id="folder-app"
              tabindex="-1"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
            <button type="button"
                    data-action="click->tree-view#toggle"
                    aria-controls="tree-cb-content-app"
                    aria-expanded="true"
                    data-state="open"
                    class="flex flex-1 items-center gap-2 text-sm outline-hidden focus:underline">
              <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
              <span class="font-medium">app</span>
            </button>
          </div>
          <!-- Grid wrapper for smooth animation -->
          <div id="tree-cb-content-app"
               data-state="open"
               role="region"
               class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
               data-tree-view-target="content">
            <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">

              <!-- File: layout.tsx -->
              <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                <input
                  type="checkbox"
                  name="files[]"
                  value="src/app/layout.tsx"
                  tabindex="-1"
                  data-checkbox-select-all-target="checkbox child"
                  data-action="click->checkbox-select-all#toggle">
                <button type="button"
                        data-tree-view-item
                        data-action="click->tree-view#selectFile"
                        class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                  <span>layout.tsx</span>
                </button>
              </div>

              <!-- File: page.tsx -->
              <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                <input
                  type="checkbox"
                  name="files[]"
                  value="src/app/page.tsx"
                  tabindex="-1"
                  data-checkbox-select-all-target="checkbox child"
                  data-action="click->checkbox-select-all#toggle">
                <button type="button"
                        data-tree-view-item
                        data-action="click->tree-view#selectFile"
                        class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                  <span>page.tsx</span>
                </button>
              </div>

              <!-- Folder "components" -->
              <div class="flex flex-col gap-y-1" data-checkbox-select-all-target="parent">
                <div class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700">
                  <input
                    type="checkbox"
                    id="folder-components"
                    tabindex="-1"
                    data-checkbox-select-all-target="checkbox child"
                    data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
                  <button type="button"
                          data-action="click->tree-view#toggle"
                          aria-controls="tree-cb-content-components"
                          aria-expanded="true"
                          data-state="open"
                          class="flex flex-1 items-center gap-2 text-sm outline-hidden focus:underline">
                    <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
                    <span class="font-medium">components</span>
                  </button>
                </div>
                <!-- Grid wrapper for smooth animation -->
                <div id="tree-cb-content-components"
                     data-state="open"
                     role="region"
                     class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
                     data-tree-view-target="content">
                  <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">

                    <!-- Folder "ui" -->
                    <div class="flex flex-col gap-y-1" data-checkbox-select-all-target="parent">
                      <div class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700">
                        <input
                          type="checkbox"
                          id="folder-ui"
                          tabindex="-1"
                          data-checkbox-select-all-target="checkbox child"
                          data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
                        <button type="button"
                                data-action="click->tree-view#toggle"
                                aria-controls="tree-cb-content-ui"
                                aria-expanded="false"
                                data-state="closed"
                                class="flex flex-1 items-center gap-2 text-sm outline-hidden focus:underline">
                          <svg data-tree-view-target="icon" class="folder-closed" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M13.75,5.25c1.105,0,2,.895,2,2v5.5c0,1.105-.895,2-2,2H4.25c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626Z"></path></g></svg>
                          <span class="font-medium">ui</span>
                        </button>
                      </div>
                      <!-- Grid wrapper for smooth animation -->
                      <div id="tree-cb-content-ui"
                           data-state="closed"
                           role="region"
                           class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
                           data-tree-view-target="content"
                           hidden>
                        <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">

                          <!-- File: button.tsx -->
                          <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                            <input
                              type="checkbox"
                              name="files[]"
                              value="src/app/components/ui/button.tsx"
                              tabindex="-1"
                              data-checkbox-select-all-target="checkbox child"
                              data-action="click->checkbox-select-all#toggle">
                            <button type="button"
                                    data-tree-view-item
                                    data-action="click->tree-view#selectFile"
                                    class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                              <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                              <span>button.tsx</span>
                            </button>
                          </div>

                          <!-- File: input.tsx -->
                          <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                            <input
                              type="checkbox"
                              name="files[]"
                              value="src/app/components/ui/input.tsx"
                              tabindex="-1"
                              data-checkbox-select-all-target="checkbox child"
                              data-action="click->checkbox-select-all#toggle">
                            <button type="button"
                                    data-tree-view-item
                                    data-action="click->tree-view#selectFile"
                                    class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                              <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                              <span>input.tsx</span>
                            </button>
                          </div>
                        </div>
                      </div>
                    </div>

                    <!-- File: header.tsx -->
                    <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                      <input
                        type="checkbox"
                        name="files[]"
                        value="src/app/components/header.tsx"
                        tabindex="-1"
                        data-checkbox-select-all-target="checkbox child"
                        data-action="click->checkbox-select-all#toggle">
                      <button type="button"
                              data-tree-view-item
                              data-action="click->tree-view#selectFile"
                              class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                        <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                        <span>header.tsx</span>
                      </button>
                    </div>

                    <!-- File: footer.tsx -->
                    <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                      <input
                        type="checkbox"
                        name="files[]"
                        value="src/app/components/footer.tsx"
                        tabindex="-1"
                        data-checkbox-select-all-target="checkbox child"
                        data-action="click->checkbox-select-all#toggle">
                      <button type="button"
                              data-tree-view-item
                              data-action="click->tree-view#selectFile"
                              class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                        <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                        <span>footer.tsx</span>
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- Folder "lib" -->
        <div class="flex flex-col gap-y-1" data-checkbox-select-all-target="parent">
          <div class="flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700">
            <input
              type="checkbox"
              id="folder-lib"
              tabindex="-1"
              data-checkbox-select-all-target="checkbox child"
              data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
            <button type="button"
                    data-action="click->tree-view#toggle"
                    aria-controls="tree-cb-content-lib"
                    aria-expanded="true"
                    data-state="open"
                    class="flex flex-1 items-center gap-2 text-sm outline-hidden focus:underline">
              <svg data-tree-view-target="icon" class="folder-open" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M5,14.75h-.75c-1.105,0-2-.895-2-2V4.75c0-1.105,.895-2,2-2h1.825c.587,0,1.144,.258,1.524,.705l1.524,1.795h4.626c1.105,0,2,.895,2,2v1"></path><path d="M16.148,13.27l.843-3.13c.257-.953-.461-1.89-1.448-1.89H6.15c-.678,0-1.272,.455-1.448,1.11l-.942,3.5c-.257,.953,.461,1.89,1.448,1.89H14.217c.904,0,1.696-.607,1.931-1.48Z"></path></g></svg>
              <span class="font-medium">lib</span>
            </button>
          </div>
          <!-- Grid wrapper for smooth animation -->
          <div id="tree-cb-content-lib"
               data-state="open"
               role="region"
               class="grid transition-[grid-template-rows] duration-300 ease-in-out data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
               data-tree-view-target="content">
            <div class="ml-4 pl-2 border-l border-neutral-200 dark:border-neutral-700 flex flex-col gap-y-1 overflow-hidden min-h-0">

              <!-- File: utils.ts -->
              <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                <input
                  type="checkbox"
                  name="files[]"
                  value="src/lib/utils.ts"
                  tabindex="-1"
                  data-checkbox-select-all-target="checkbox child"
                  data-action="click->checkbox-select-all#toggle">
                <button type="button"
                        data-tree-view-item
                        data-action="click->tree-view#selectFile"
                        class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                  <span>utils.ts</span>
                </button>
              </div>

              <!-- File: constants.ts -->
              <div class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-neutral-600 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-700/50 has-[:checked]:bg-neutral-200 dark:has-[:checked]:bg-neutral-700 has-[:checked]:text-neutral-900 dark:has-[:checked]:text-neutral-50">
                <input
                  type="checkbox"
                  name="files[]"
                  value="src/lib/constants.ts"
                  tabindex="-1"
                  data-checkbox-select-all-target="checkbox child"
                  data-action="click->checkbox-select-all#toggle">
                <button type="button"
                        data-tree-view-item
                        data-action="click->tree-view#selectFile"
                        class="flex flex-1 items-center gap-2 outline-hidden focus:underline">
                  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M15.16,6.25h-3.41c-.552,0-1-.448-1-1V1.852"></path><path d="M2.75,14.25V3.75c0-1.105,.895-2,2-2h5.586c.265,0,.52,.105,.707,.293l3.914,3.914c.188,.188,.293,.442,.293,.707v7.586c0,1.105-.895,2-2,2H4.75c-1.105,0-2-.895-2-2Z"></path></g></svg>
                  <span>constants.ts</span>
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Configuration

The tree view component is powered by a Stimulus controller that provides keyboard navigation, smooth animations, and flexible folder/file management.

Controller Setup

Basic tree view structure with required data attributes:

<div data-controller="tree-view" data-tree-view-animate-value="true">
  <div class="overflow-hidden">
    <button type="button"
            data-action="click->tree-view#toggle"
            aria-controls="tree-content-folder"
            aria-expanded="false"
            data-state="closed">
      <svg data-tree-view-target="icon"><!-- Folder icon --></svg>
      <span>Folder Name</span>
    </button>
    <div id="tree-content-folder"
         data-state="closed"
         role="region"
         data-tree-view-target="content"
         hidden>
      <!-- Nested content -->
    </div>
  </div>
</div>

Configuration Values

Prop Description Type Default
animate
Controls whether folder expand/collapse animations are enabled Boolean true

Targets

Target Description Required
icon
The folder icon that changes between open and closed states Optional
content
The collapsible content area containing nested items Required

Actions

Action Description Usage
toggle
Toggles the open/closed state of a folder in the tree data-action="click->tree-view#toggle"

Required Attributes

Attribute Description Example
aria-controls Links the button to its content area aria-controls="tree-content-folder"
aria-expanded Indicates whether the folder is open (true) or closed (false) aria-expanded="false"
data-state Visual state indicator for styling data-state="closed"
role Accessibility role for content areas role="region"
hidden Hides closed content from screen readers and layout hidden

Accessibility Features

  • Keyboard Navigation: Use arrows to navigate between folders
  • ARIA Support: Proper aria-expanded and aria-controls attributes
  • Screen Reader Friendly: Semantic regions and proper focus management
  • Keyboard Actions: Enter and Space to toggle folders

Icon Management

The controller automatically handles folder icon transitions:

  • Closed Folders: Display a closed folder icon
  • Open Folders: Display an open folder icon with papers/documents
  • Automatic Switching: Icons change automatically based on folder state

Animation Control

The tree view supports smooth height animations:

  • Smooth Expand: Content animates from height 0 to natural height
  • Smooth Collapse: Content animates from natural height to 0
  • Disable Animations: Set data-tree-view-animate-value="false" for instant toggling

Styling Classes

Key CSS classes used in the component:

  • overflow-hidden: Required for smooth height animations
  • transition-[height]: Enables height transition animations
  • border-l: Creates visual hierarchy lines
  • ml-4 pl-2: Creates proper indentation for nested items

Usage Notes

  • Each folder button must have a unique aria-controls value
  • Content areas must have matching id attributes
  • Use data-state="open" to have folders start expanded
  • File items don't need the toggle action, only folder items do
  • The controller handles keyboard navigation automatically for all clickable elements

Tree View with Checkbox Selection

Combine the tree-view controller with the checkbox-select-all controller to enable file/folder selection. This pattern is perfect for file managers, permission systems, or any hierarchical selection UI.

Combined Controller Setup

Add both controllers to the root element:

<div data-controller="tree-view checkbox-select-all"
     data-tree-view-animate-value="true"
     data-checkbox-select-all-toggle-key-value="x">

  <!-- Select All Header -->
  <input type="checkbox"
         data-checkbox-select-all-target="selectAll"
         data-action="click->checkbox-select-all#toggleAll">

  <!-- Folder with Checkbox -->
  <div data-checkbox-select-all-target="parent">
    <div class="flex items-center gap-2">
      <input type="checkbox"
             data-checkbox-select-all-target="checkbox"
             data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle">
      <button data-action="click->tree-view#toggle"
              aria-controls="folder-content"
              aria-expanded="false">
        <svg data-tree-view-target="icon"><!-- Folder icon --></svg>
        <span>Folder Name</span>
      </button>
    </div>
    <div id="folder-content" data-tree-view-target="content" hidden>
      <!-- Child files with checkboxes -->
      <input type="checkbox"
             data-checkbox-select-all-target="checkbox child"
             data-action="click->checkbox-select-all#toggle">
    </div>
  </div>
</div>

Key Patterns for Combined Usage

Pattern Description
Separate checkbox and toggle Keep the checkbox separate from the folder toggle button so clicking the checkbox doesn't expand/collapse the folder
Parent wrapper Wrap each folder in data-checkbox-select-all-target="parent" to enable parent/child checkbox relationships
Folder checkbox actions Use both toggleChildren and toggle actions on folder checkboxes: data-action="click->checkbox-select-all#toggleChildren click->checkbox-select-all#toggle"
Child checkbox targeting Mark child checkboxes with both checkbox and child targets: data-checkbox-select-all-target="checkbox child"
Visual hierarchy Use border-l and indentation classes on content areas, with has-[:checked] for active state styling
Additional Controller Required
The tree view with checkboxes example requires the checkbox-select-all controller in addition to the tree-view controller. Make sure to install both controllers in your project.

Table of contents

Get notified when new components come out