Tree

Component

Interactive examples and API documentation

Basic Usage
Basic usage with checkable, selectable, disabled nodes, and default keys.
Code
<% nodes = [
  {
    key: "0-0",
    title: "Parent Node",
    children: [
      { key: "0-0-0", title: "Child Node", checkable: true },
      { key: "0-0-1", title: "Disabled Child", disabled: true }
    ]
  },
  {
    key: "0-1",
    title: "Selectable Disabled",
    disabled: true
  },
  {
    key: "0-2",
    title: "Not Selectable",
    selectable: false
  }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  checkable: true,
  selectable: true,
  default_expand_keys: ["0-0"],
  default_selected_keys: ["0-0-0"],
  default_checked_keys: ["0-0-1"]
) %>
Controlled Tree
Controlled checked keys with parent-child aggregation.
Checked keys: []
Code
<% nodes = [
  {
    key: "0-0",
    title: "Design System",
    children: [
      { key: "0-0-0", title: "Tokens" },
      { key: "0-0-1", title: "Components" },
      { key: "0-0-2", title: "Guidelines" }
    ]
  },
  {
    key: "0-1",
    title: "Product",
    children: [
      { key: "0-1-0", title: "Roadmap" },
      { key: "0-1-1", title: "Specs" }
    ]
  }
] %>

<%= render Hakumi::Space::Component.new(direction: :vertical, size: :middle) do %>
  <%= render Hakumi::Tree::Component.new(
    id: "controlled-tree",
    nodes: nodes,
    checkable: true,
    default_expand_keys: ["0-0"],
    default_checked_keys: ["0-0-0"]
  ) %>

  <%= render Hakumi::Typography::Text::Component.new(id: "controlled-tree-output", type: :secondary) do %>
    Checked keys: []
  <% end %>
<% end %>

<script>
  (() => {
    const treeEl = document.getElementById("controlled-tree")
    const output = document.getElementById("controlled-tree-output")
    if (!treeEl || !output) return

    let wired = false

    const wire = () => {
      const api = treeEl.hakumiTree
      if (!api) return false

      const update = () => {
        output.textContent = `Checked keys: ${JSON.stringify(api.getCheckedKeys())}`
      }

      treeEl.addEventListener("hakumi--tree:check", update)
      update()
      wired = true
      return true
    }

    if (wire()) return

    const interval = setInterval(() => {
      if (wired) return
      if (wire()) clearInterval(interval)
    }, 100)

    setTimeout(() => clearInterval(interval), 5000)
  })()
</script>
Draggable
Drag tree nodes to reorder or move between parents.
Code
<% nodes = [
  {
    key: "0-0",
    title: "Backlog",
    children: [
      { key: "0-0-0", title: "Task A" },
      { key: "0-0-1", title: "Task B" }
    ]
  },
  {
    key: "0-1",
    title: "In Progress",
    children: [
      { key: "0-1-0", title: "Task C" }
    ]
  }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  draggable: true,
  default_expand_keys: ["0-0", "0-1"]
) %>
Load Data Asynchronously
Load nodes on expand using the public API.
Code
<% nodes = [
  { key: "0-0", title: "Root", async: true },
  { key: "0-1", title: "Static", children: [
      { key: "0-1-0", title: "Child" }
    ]
  }
] %>

<%= render Hakumi::Tree::Component.new(
  id: "async-tree",
  nodes: nodes,
  default_expand_keys: ["0-1"]
) %>

<script>
  (() => {
    const treeEl = document.getElementById("async-tree")
    if (!treeEl) return

    let wired = false

    const wire = () => {
      const api = treeEl.hakumiTree
      if (!api) return false

      treeEl.addEventListener("hakumi--tree:load", (event) => {
        const { key } = event.detail

        setTimeout(() => {
          api.addNodes(key, [
            { key: `${key}-0`, title: "Loaded Node 1" },
            { key: `${key}-1`, title: "Loaded Node 2" }
          ])
          api.setLoading(key, false)
        }, 600)
      })

      wired = true
      return true
    }

    if (wire()) return

    const interval = setInterval(() => {
      if (wired) return
      if (wire()) clearInterval(interval)
    }, 100)

    setTimeout(() => clearInterval(interval), 5000)
  })()
</script>
Searchable
Search nodes by title with highlight and auto-expand.
Code
<% nodes = [
  { key: "0-0", title: "Design", children: [
      { key: "0-0-0", title: "Tokens" },
      { key: "0-0-1", title: "Components" }
    ]
  },
  { key: "0-1", title: "Engineering", children: [
      { key: "0-1-0", title: "Rails" },
      { key: "0-1-1", title: "Stimulus" }
    ]
  }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  searchable: true,
  default_expand_keys: ["0-0", "0-1"]
) %>
Tree with Line
Show connecting lines and customize the preset switcher icon.
Code
<% nodes = [
  { key: "0-0", title: "Folders", children: [
      { key: "0-0-0", title: "Design" },
      { key: "0-0-1", title: "Assets" }
    ]
  },
  { key: "0-1", title: "Docs" }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  show_line: true,
  default_expand_keys: ["0-0"],
  switcher_icon: :plus_square
) %>
Customize Icon
Customize icons per node.
Code
<% nodes = [
  { key: "0-0", title: "Projects", icon: :folder, children: [
      { key: "0-0-0", title: "Specs", icon: :file },
      { key: "0-0-1", title: "Roadmap", icon: :calendar }
    ]
  },
  { key: "0-1", title: "Settings", icon: :setting }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  default_expand_keys: ["0-0"]
) %>
Customize Collapse/Expand Icon
Customize collapse/expand icon for tree nodes.
Code
<% nodes = [
  { key: "0-0", title: "Overview", children: [
      { key: "0-0-0", title: "Summary" },
      { key: "0-0-1", title: "Details" }
    ]
  }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  default_expand_keys: ["0-0"],
  switcher_icon: :plus
) %>
Directory Tree
Directory tree with multi-select via ctrl/command.
Code
<% nodes = [
  { key: "0-0", title: "src", children: [
      { key: "0-0-0", title: "components" },
      { key: "0-0-1", title: "controllers" }
    ]
  },
  { key: "0-1", title: "README.md", is_leaf: true }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  directory: true,
  multiple: true,
  default_expand_keys: ["0-0"]
) %>
Block Node
Block-level clickable nodes.
Code
<% nodes = [
  { key: "0-0", title: "Full-width node", children: [
      { key: "0-0-0", title: "Child A" },
      { key: "0-0-1", title: "Child B" }
    ]
  }
] %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  block_node: true,
  default_expand_keys: ["0-0"]
) %>
Virtual Scroll
Virtual list scrolling via height prop.
Code
<% nodes = (1..20).map do |index|
  {
    key: "0-#{index}",
    title: "Node #{index}",
    children: (1..4).map { |child| { key: "0-#{index}-#{child}", title: "Node #{index}.#{child}" } }
  }
end %>

<%= render Hakumi::Tree::Component.new(
  nodes: nodes,
  height: 240,
  default_expand_keys: ["0-1"]
) %>
Programmatic Rendering
Render Tree via HakumiComponents.renderComponent().
Code
<%= render Hakumi::Space::Component.new(size: :middle) do %>
  <%= render(Hakumi::Button::Component.new(type: :primary, id: "programmatic-tree-btn")) { "Render Tree via API" } %>
  <%= render Hakumi::Container::Component.new(id: "programmatic-tree-target", width: :fluid, padded: false, centered: false) %>
<% end %>

<script>
  (() => {
    const button = document.getElementById("programmatic-tree-btn")
    if (!button) return

    const wire = () => {
      if (!window.HakumiComponents?.renderComponent) return false

      button.addEventListener("click", async () => {
        await window.HakumiComponents.renderComponent("tree", {
          params: {
            nodes: JSON.stringify([
              { key: "0-0", title: "Programmatic", children: [
                  { key: "0-0-0", title: "Dynamic" }
                ]
              }
            ]),
            default_expand_keys: JSON.stringify(["0-0"])
          },
          target: "#programmatic-tree-target"
        })
      })

      return true
    }

    if (wire()) return

    const onReady = () => {
      if (wire()) {
        document.removeEventListener("turbo:load", onReady)
        window.removeEventListener("load", onReady)
      }
    }

    document.addEventListener("turbo:load", onReady)
    window.addEventListener("load", onReady)
  })()
</script>

Tree API

Prop Type Default Description
nodes Array of Hashes [] Tree data with keys, titles, and children.
checkable Boolean false Show checkboxes.
selectable Boolean true Allow selecting nodes.
disabled Boolean false Disable the entire tree.
default_expand_keys Array[String] [] Keys expanded by default.
default_selected_keys Array[String] [] Keys selected by default.
default_checked_keys Array[String] [] Keys checked by default.
expanded_keys Array[String] or nil nil Controlled expanded keys.
selected_keys Array[String] or nil nil Controlled selected keys.
checked_keys Array[String] or nil nil Controlled checked keys.
show_line Boolean false Show connecting lines between nodes.
switcher_icon String or Symbol nil Custom switcher icon name.
icon String or Symbol nil Default node icon.
directory Boolean false Enable directory tree style.
multiple Boolean false Allow multi-selection.
draggable Boolean false Enable drag and drop.
block_node Boolean false Make nodes block-level clickable.
searchable Boolean false Show search input.
search_placeholder String Search Placeholder text for search input.
height Number nil Max height for virtual scroll container.
check_strictly Boolean false Disable parent-child checking cascade.
indent Number 24 Indent size in pixels.
**html_options Keyword args - Extra attributes merged into the wrapper.

JavaScript API (element.hakumiTree)

Prop Type Default Description
getSelectedKeys() Function - Return selected keys.
setSelectedKeys(keys) Function - Set selected keys.
getCheckedKeys() Function - Return checked keys.
setCheckedKeys(keys) Function - Set checked keys.
getHalfCheckedKeys() Function - Return half-checked keys.
getExpandedKeys() Function - Return expanded keys.
setExpandedKeys(keys) Function - Set expanded keys.
expandAll() Function - Expand all nodes.
collapseAll() Function - Collapse all nodes.
toggleNode(key) Function - Toggle node expansion by key.
selectNode(key, options) Function - Select a node programmatically.
checkNode(key, checked) Function - Check/uncheck a node programmatically.
addNodes(parentKey, nodes) Function - Append child nodes to a parent key.
setLoading(key, loading) Function - Toggle async loading state.

Events

Prop Type Default Description
hakumi--tree:select CustomEvent - Triggered when selection changes.
hakumi--tree:check CustomEvent - Triggered when checked keys change.
hakumi--tree:expand CustomEvent - Triggered when expanded keys change.
hakumi--tree:load CustomEvent - Triggered when async node needs data.
hakumi--tree:dragEnd CustomEvent - Triggered after drag and drop.