Taming Hierarchy in the UI Jungle

Tree views are everywhere—navigation menus, page builders, file explorers, site maps.

But if you’ve ever tried building one from scratch, you know it’s deceptively complex:

  • Drag-and-drop without breaking structure

  • Lazy loading nested nodes

  • Keyboard accessibility

  • Visual customization

  • State management

Most solutions are tightly bound to a framework (React or Vue), often fragile under large datasets, or simply not extensible.

I wanted something different.
So I built Node Explorer—a web component that solves all of this while staying framework-agnostic and fully customizable.


Introducing: Node Explorer

Node Explorer is a standalone, modern web component that offers:

  • Infinite nesting of tree nodes
  • Native drag-and-drop support
  • API event system for control (select, expand, drop, etc.)
  • Material Design icon support
  • Accessible by keyboard
  • Fully themeable (dark, light, minimal, high contrast)
  • Works with React, Vue, Angular, or plain JavaScript

And it’s open source.
Let me show you how to use it in a real Vue 3 + TypeScript project.


Installation

npm install @jmkcoder/components

Vue 3 + TypeScript Integration

Take your content tree or CMS editor to the next level with a native-feeling file explorer in just a few lines of code.

Here's what the result looks like when embedded in your app

Button Preview

Click the image to try the live, interactive component in Storybook!

Lets break it down


📦 Step 1: Import the Component

Lets import the styles and component bundle in your App.vue file

import '@jmkcoder/components/dist/components.bundle.css'
import '@jmkcoder/components/dist/index.js'

🧩 Step 2: Wrap in a Local Component

Use the odyssey-node-explorer as a native Web Component inside a Vue wrapper

<template>
  
    ref="explorer"
    :allow-drag-drop="allowDragDrop"
    :allow-multi-select="allowMultiSelect"
    :theme="theme"
    :compact="compact"
  />
template>

<script lang="ts">
import { defineComponent, ref, watch, onMounted, onBeforeUnmount } from 'vue'

export default defineComponent({
  name: 'NodeExplorer',
  props: {
    nodes: Array,
    allowDragDrop: { type: Boolean, default: true },
    allowMultiSelect: { type: Boolean, default: false },
    theme: { type: String, default: 'light' },
    compact: { type: Boolean, default: false },
  },
  emits: ['node-selected'],
  setup(props, { emit }) {
    const explorer = ref<HTMLElement | null>(null)

    const updateNodes = () => {
      if (explorer.value) {
        explorer.value.setAttribute('nodes', JSON.stringify(props.nodes))
      }
    }

    const handleNodeSelected = (event: CustomEvent) => {
      emit('node-selected', event)
    }

    watch(() => props.nodes, updateNodes)
    watch(() => props.compact, (newVal) => {
      explorer.value?.setAttribute('compact', newVal ? 'true' : 'false')
    })

    onMounted(() => {
      updateNodes()
      explorer.value?.addEventListener('node-selected', handleNodeSelected)
    })

    onBeforeUnmount(() => {
      explorer.value?.removeEventListener('node-selected', handleNodeSelected)
    })

    return { explorer }
  },
})
script>

🏡 Step 3: Use It in a View

Pass your dynamic data into the explorer with simple bindings.

<template>
  
    :nodes="treeData"
    theme="dark"
    @node-selected="onNodeSelected"
  />
template>

<script setup lang="ts">
import NodeExplorer from '../components/NodeExplorer.vue'
import type { ExplorerNode } from '@jmkcoder/components/dist/components/node-explorer/node-explorer.type'

const treeData: ExplorerNode[] = [
  {
    id: 'root',
    label: 'My Project',
    icon: 'folder',
    expanded: true,
    children: [
      {
        id: 'src',
        label: 'src',
        icon: 'folder',
        children: [
          {
            id: 'components',
            label: 'components',
            icon: 'folder',
            children: [
              { id: 'button.js', label: 'button.js', icon: 'javascript' },
              { id: 'input.js', label: 'input.js', icon: 'javascript' },
            ],
          },
          { id: 'app.js', label: 'app.js', icon: 'javascript' },
        ],
      },
    ],
  },
]

const onNodeSelected = (event: CustomEvent) => {
  const selectedNode = event.detail.node as ExplorerNode
  console.log('Selected:', selectedNode.label)
}
script>

🎨 Customizing the Look

Switch between light/dark themes instantly:

theme="dark" />

Or go fully custom using CSS variables:

odyssey-node-explorer {
  --primary-color: #635BFF;      /* Accent color */
  --primary-light: rgba(99, 91, 255, 0.08); /* Lighter accent */
  --gray-light: #F7FAFC;         /* Background for headers */
  --gray-border: #E8E8E8;        /* Border colors */
  --hover-bg: #F5F7FA;           /* Hover background */
  --text-secondary: #697386;     /* Secondary text color */
  --text-primary: #1A1F36;       /* Primary text color */
  --transition-duration: 200ms;  /* Animation speed */
  --connector-width: 2px;        /* Width of the connector lines */
  --connector-color: #E8E8E8;    /* Color of connector lines */
}

API Events You Can Hook Into

Event Name Description
load-children Fired when a lazy-loaded node is expanded and needs its children
node-selected Fired when a node is selected
nodes-selected Fired when multiple nodes are selected (when multi-select is enabled)
nodes-changed Fired when nodes structure changes (drag-drop, expand/collapse)
node-expanded Fired when a node is expanded
node-collapsed Fired when a node is collapsed
drag-start Fired when a drag operation starts
drag-over Fired during a drag operation
drag-leave Fired when a drag operation leaves a target
drop Fired when a node is dropped

These events give you fine-grained control over how you react to user actions, whether you're syncing state to Vuex, calling an API, or just updating UI.


Why Node Explorer is Different

There are many libraries and plugins that try to solve this.

Node Explorer does it while being:

  • Framework-agnostic: works with anything
  • Composable: expose only what you need via API
  • Extensible: add custom data, event handlers, even logic layers
  • Production-ready: optimized rendering and accessibility

Getting Started


Let’s Build Together

This is just the beginning.

Node Explorer will be powering more plugins, editors, and UIs as I work on a project I call Odyssey.

If you’re building something that requires dynamic hierarchy—give this component a try.

If it sparks ideas or could use improvements, I’d love your feedback.

Star it. Fork it. Use it.

Let’s explore what’s possible, together.