A high-performance, feature-rich hierarchical tree view component for Svelte 5 with drag & drop support, search functionality, and flexible data structures using LTree.
Browse interactive code examples and the full API reference at svelte-treeview.keenmate.dev
Note
New virtual scroll mode renders only visible nodes — handle 50,000+ items without DOM bloat.
Three rendering modes, one consistent look:
| Mode | Best for | How it works |
|---|---|---|
| Recursive | Small trees (<500 nodes) | Traditional nested Svelte components |
| Progressive (default) | Medium trees (500–10,000) | Flat rendering with exponential batching |
| Virtual Scroll | Large trees (10,000+) | Only visible rows + overscan are in the DOM |
<Tree
{data}
virtualScroll={true}
virtualRowHeight={28}
virtualOverscan={5}
virtualContainerHeight="600px"
/>Also in this release:
- Search result navigation — dual-mode filter/search with result counter, prev/next (Enter/Shift+Enter), round-robin cycling
- Floating drop zones auto-expand when positions are restricted — pure CSS
:not(:has()), zero JS overhead - Cross-tree drop positioning fixed — above/below placement now works correctly between trees
- Unified indentation & gaps across all three rendering modes
- Drag & drop disabled by default — set
dragDropMode="both"to enable
Note
You can now restrict which drop positions (above/below/child) are allowed per node.
Use getAllowedDropPositionsCallback for dynamic logic or allowedDropPositionsMember for server data:
// Files can only have siblings, trash only accepts children
function getAllowedDropPositions(node) {
if (node.data?.type === 'file') return ['above', 'below'];
if (node.data?.type === 'trash') return ['child'];
return undefined; // all positions allowed (default)
}Note
The tree now uses progressive flat rendering by default for significantly improved performance.
What this means:
- The tree renders immediately with the first batch of nodes (~20 by default)
- Remaining nodes are rendered progressively in subsequent frames
- For large trees (5000+ nodes), you'll see nodes appear over ~100-500ms instead of a single long freeze
- The UI remains responsive during rendering
Configuration options:
<Tree
{data}
useFlatRendering={true} <!-- Default: true (flat mode) -->
progressiveRender={true} <!-- Default: true (batched rendering) -->
initialBatchSize={20} <!-- First batch size (default: 20) -->
maxBatchSize={500} <!-- Maximum batch size cap (default: 500) -->
/>Exponential batching: The first batch renders 20 nodes instantly, then doubles each frame (20 → 40 → 80 → 160 → 320 → 500...) for optimal perceived performance.
To use the legacy recursive rendering:
<Tree
{data}
useFlatRendering={false} <!-- Uses recursive Node components -->
/>Recursive mode may be preferred for very small trees or when you need the {#key changeTracker} behavior that recreates all nodes on any change.
- Svelte 5 Native: Built specifically for Svelte 5 with full support for runes and modern Svelte patterns
- High Performance: Flat rendering mode with progressive loading for 5000+ nodes
- Drag & Drop: Built-in drag and drop with position control (above/below/child), touch support, and async validation
- Tree Editing: Built-in methods for add, move, remove operations with automatic path management
- Search & Filter: Integrated FlexSearch for fast, full-text search capabilities
- Flexible Data Sources: Works with any hierarchical data structure
- Context Menus: Dynamic right-click menus with callback-based generation, icons, disabled states
- Visual Customization: Extensive styling options and icon customization
- TypeScript Support: Full TypeScript support with comprehensive type definitions
- Accessibility: Built with accessibility in mind
npm install @keenmate/svelte-treeviewThe component requires CSS to display correctly. Import the styles in your app:
JavaScript import (in your main.js/main.ts or Vite/Webpack entry):
import '@keenmate/svelte-treeview/styles.scss';Svelte component import:
<style>
@import '@keenmate/svelte-treeview/styles.scss';
</style><script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
const data = [
{ path: '1', name: 'Documents', type: 'folder' },
{ path: '1.1', name: 'Projects', type: 'folder' },
{ path: '1.1.1', name: 'Project A', type: 'folder' },
{ path: '1.1.2', name: 'Project B', type: 'folder' },
{ path: '2', name: 'Pictures', type: 'folder' },
{ path: '2.1', name: 'Vacation', type: 'folder' }
];
</script>
<Tree
{data}
idMember="path"
pathMember="path"
displayValueMember="name"
/>Tip
Performance tip: When passing large arrays (1000+ items) to the Tree component, use $state.raw() instead of $state() to avoid severe performance issues. Svelte 5's $state() creates deep proxies — with thousands of items this causes up to 5,000x slowdown. The array itself remains reactive; only individual items lose deep reactivity (which Tree doesn't need).
// BAD - Each item becomes a Proxy
let treeData = $state<TreeNode[]>([])
// GOOD - Items remain plain objects
let treeData = $state.raw<TreeNode[]>([])<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
const fileData = [
{ path: '1', name: 'Documents', type: 'folder', icon: '📁' },
{ path: '1.1', name: 'report.pdf', type: 'file', icon: '📄', size: '2.3 MB' },
{ path: '2', name: 'Images', type: 'folder', icon: '🖼️' },
{ path: '2.1', name: 'photo.jpg', type: 'file', icon: '🖼️', size: '1.8 MB' }
];
</script>
<Tree
data={fileData}
idMember="path"
pathMember="path"
selectedNodeClass="ltree-selected-bold"
onNodeClicked={(node) => console.log('Clicked:', node.data.name)}
>
{#snippet nodeTemplate(node)}
<div class="d-flex align-items-center">
<span class="me-2">{node.data.icon}</span>
<strong>{node.data.name}</strong>
{#if node.data.size}
<small class="text-muted ms-2">({node.data.size})</small>
{/if}
</div>
{/snippet}
</Tree><script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
let searchText = $state('');
const data = [/* your data */];
</script>
<input
type="text"
placeholder="Search..."
bind:value={searchText}
/>
<Tree
{data}
idMember="path"
pathMember="path"
shouldUseInternalSearchIndex={true}
searchValueMember="name"
bind:searchText
/><script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
import type { SearchOptions } from 'flexsearch';
let treeRef;
const data = [/* your data */];
// Programmatic search with FlexSearch options
function performAdvancedSearch(searchTerm: string) {
const searchOptions: SearchOptions = {
suggest: true, // Enable suggestions for typos
limit: 10, // Limit results to 10 items
bool: "and" // Use AND logic for multiple terms
};
const results = treeRef.searchNodes(searchTerm, searchOptions);
console.log('Advanced search results:', results);
}
// Programmatic filtering with options
function filterWithOptions(searchTerm: string) {
const searchOptions: SearchOptions = {
threshold: 0.8, // Similarity threshold
depth: 2 // Search depth
};
treeRef.filterNodes(searchTerm, searchOptions);
}
</script>
<Tree
bind:this={treeRef}
{data}
idMember="path"
pathMember="path"
shouldUseInternalSearchIndex={true}
searchValueMember="name"
/>
<button onclick={() => performAdvancedSearch('document')}>
Advanced Search
</button>
<button onclick={() => filterWithOptions('project')}>
Filter with Options
</button>The searchOptions parameter accepts any options supported by FlexSearch. Common options include:
| Option | Type | Description | Example |
|---|---|---|---|
suggest |
boolean |
Enable suggestions for typos | { suggest: true } |
limit |
number |
Maximum number of results | { limit: 10 } |
threshold |
number |
Similarity threshold (0-1) | { threshold: 0.8 } |
depth |
number |
Search depth for nested content | { depth: 2 } |
bool |
string |
Boolean logic: "and", "or" | { bool: "and" } |
where |
object |
Filter by field values | { where: { type: "folder" } } |
For complete FlexSearch documentation, visit: FlexSearch Options
Note: Drag and drop is disabled by default. Set dragDropMode to enable it.
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
let treeRef: Tree<MyNode>;
const data = [
{ path: '1', name: 'Folder 1' },
{ path: '1.1', name: 'Item 1' },
{ path: '2', name: 'Folder 2' }
];
function onDragStart(node, event) {
console.log('Dragging:', node.data.name);
}
// Same-tree moves are auto-handled - this callback is for notification/custom logic
function onDrop(dropNode, draggedNode, position, event, operation) {
console.log(`Dropped ${draggedNode.data.name} ${position} ${dropNode?.data.name}`);
// position is 'above', 'below', or 'child'
// operation is 'move' or 'copy' (Ctrl+drag)
}
</script>
<Tree
bind:this={treeRef}
{data}
idMember="path"
pathMember="path"
dragDropMode="both"
orderMember="sortOrder"
dragOverNodeClass="ltree-dragover-highlight"
onNodeDragStart={onDragStart}
onNodeDrop={onDrop}
/>When using dropZoneMode="floating" (default), users can choose where to drop:
- Above: Insert as sibling before the target node
- Below: Insert as sibling after the target node
- Child: Insert as child of the target node
You can restrict which drop positions are allowed per node. This is useful for:
- Trash/Recycle Bin: Only allow dropping INTO (child), not above/below
- Files: Only allow above/below (can't drop INTO a file)
- Folders: Allow all positions (default)
<script lang="ts">
import { Tree, type DropPosition, type LTreeNode } from '@keenmate/svelte-treeview';
// Dynamic callback approach
function getAllowedDropPositions(node: LTreeNode<MyItem>): DropPosition[] | null {
if (node.data?.type === 'file') return ['above', 'below'];
if (node.data?.type === 'trash') return ['child'];
return undefined; // all positions allowed
}
</script>
<Tree
{data}
getAllowedDropPositionsCallback={getAllowedDropPositions}
/>Or use the member approach for server-side data:
<Tree
{data}
allowedDropPositionsMember="allowedDropPositions"
/>
<!-- Where data items have: { allowedDropPositions: ['child'] } -->When restrictions are applied:
- Glow mode: Snaps to the nearest allowed position
- Floating mode: Only renders buttons for allowed positions
Use beforeDropCallback to validate or modify drops, including async operations like confirmation dialogs:
<script lang="ts">
async function beforeDrop(dropNode, draggedNode, position, event, operation) {
// Cancel specific drops
if (draggedNode.data.locked) {
return false; // Cancel the drop
}
// Show confirmation dialog (async)
if (position === 'child' && !dropNode.data.isFolder) {
const confirmed = await showConfirmDialog('Drop as sibling instead?');
if (!confirmed) return false;
return { position: 'below' }; // Override position
}
// Proceed normally
return true;
}
</script>
<Tree
{data}
beforeDropCallback={beforeDrop}
onNodeDrop={onDrop}
/>The tree provides built-in methods for programmatic editing:
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
let treeRef: Tree<MyNode>;
// Add a new node
function addChild() {
const result = treeRef.addNode(
selectedNode?.path || '', // parent path (empty = root)
{ id: Date.now(), path: '', name: 'New Item', sortOrder: 100 }
);
if (result.success) {
console.log('Added:', result.node);
}
}
// Move a node
function moveUp() {
const siblings = treeRef.getSiblings(selectedNode.path);
const index = siblings.findIndex(s => s.path === selectedNode.path);
if (index > 0) {
treeRef.moveNode(selectedNode.path, siblings[index - 1].path, 'above');
}
}
// Remove a node
function remove() {
treeRef.removeNode(selectedNode.path);
}
</script>
<Tree
bind:this={treeRef}
{data}
idMember="id"
pathMember="path"
orderMember="sortOrder"
/>Note: When using orderMember, the tree automatically calculates sort order values when moving nodes with 'above' or 'below' positions.
The tree supports context menus with two approaches: callback-based (recommended) and snippet-based.
<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
import type { ContextMenuItem } from '@keenmate/svelte-treeview';
const data = [
{ path: '1', name: 'Documents', type: 'folder', canEdit: true, canDelete: true },
{ path: '1.1', name: 'report.pdf', type: 'file', canEdit: true, canDelete: false },
{ path: '2', name: 'Images', type: 'folder', canEdit: false, canDelete: true }
];
function createContextMenu(node): ContextMenuItem[] {
const items: ContextMenuItem[] = [];
// Always available
items.push({
icon: '📂',
title: 'Open',
callback: () => alert(`Opening ${node.data.name}`)
});
// Conditional actions based on node data
if (node.data.canEdit) {
items.push({
icon: '✏️',
title: 'Edit',
callback: () => alert(`Editing ${node.data.name}`)
});
}
if (node.data.canDelete) {
items.push({
icon: '🗑️',
title: 'Delete',
callback: () => confirm(`Delete ${node.data.name}?`) && alert('Deleted!')
});
}
// Divider
items.push({ isDivider: true });
// Disabled item example
items.push({
icon: '🔒',
title: 'Restricted Action',
isDisabled: true,
callback: () => {}
});
return items;
}
</script>
<Tree
{data}
idMember="path"
pathMember="path"
contextMenuCallback={createContextMenu}
contextMenuXOffset={8}
contextMenuYOffset={0}
/><Tree
{data}
idMember="path"
pathMember="path"
>
{#snippet contextMenu(node, closeMenu)}
<div class="context-menu-item" onclick={() => { alert(`Open ${node.data.name}`); closeMenu(); }}>
📂 Open
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick={() => { alert(`Delete ${node.data.name}`); closeMenu(); }}>
🗑️ Delete
</div>
{/snippet}
</Tree>- Dynamic menus: Generate menu items based on node properties
- Icons and dividers: Visual organization and identification
- Disabled states: Context-sensitive menu availability
- Position offset:
contextMenuXOffset/contextMenuYOffsetfor cursor clearance - Auto-close: Closes on scroll, click outside, or programmatically
- Type safety: Full TypeScript support with
ContextMenuIteminterface
The component comes with default styles that provide a clean, modern look. You can customize it extensively:
The component uses CSS custom properties for easy theming:
:root {
--tree-node-indent-per-level: 0.5rem; /* Controls indentation for each hierarchy level */
--ltree-primary: #0d6efd;
--ltree-primary-rgb: 13, 110, 253;
--ltree-success: #198754;
--ltree-success-rgb: 25, 135, 84;
--ltree-danger: #dc3545;
--ltree-danger-rgb: 220, 53, 69;
--ltree-light: #f8f9fa;
--ltree-border: #dee2e6;
--ltree-body-color: #212529;
}Note: The --tree-node-indent-per-level variable controls the consistent indentation applied at each hierarchy level. Each nested level receives this fixed indent amount, creating proper visual hierarchy without exponential indentation growth.
If you're building the styles from SCSS source, you can override these variables:
// Import your overrides before the library styles
$tree-node-indent-per-level: 1rem;
$tree-node-font-family: 'Custom Font', sans-serif;
$primary-color: #custom-color;
@import '@keenmate/svelte-treeview/styles.scss';.ltree-tree- Main tree container.ltree-node- Individual node container.ltree-node-content- Node content area.ltree-toggle-icon- Expand/collapse icons.ltree-selected-*- Selected node styles.ltree-dragover-*- Drag-over node styles.ltree-draggable- Draggable nodes.ltree-context-menu- Context menu styling.ltree-drag-over- Applied during drag operations.ltree-drop-valid/.ltree-drop-invalid- Drop target validation
The component includes several pre-built classes for styling selected nodes:
<Tree
{data}
idMember="path"
pathMember="path"
selectedNodeClass="ltree-selected-bold"
/>Available Selected Node Classes:
| Class | Description | Visual Effect |
|---|---|---|
ltree-selected-bold |
Bold text with primary color | Bold text in theme primary color |
ltree-selected-border |
Border and background highlight | Solid border with light background |
ltree-selected-brackets |
Decorative brackets around text | > Node Text < |
Available Drag-over Node Classes:
| Class | Description | Visual Effect |
|---|---|---|
ltree-dragover-highlight |
Dashed border with success color background | Green dashed border with subtle background |
ltree-dragover-glow |
Blue glow effect | Glowing shadow effect with primary color theme |
<Tree
{data}
idMember="path"
pathMember="path"
expandIconClass="custom-expand-icon"
collapseIconClass="custom-collapse-icon"
leafIconClass="custom-leaf-icon"
/>| Prop | Type | Default | Description |
|---|---|---|---|
data |
T[] |
required | Array of data objects |
idMember |
string |
required | Property name for unique identifiers |
pathMember |
string |
required | Property name for hierarchical paths |
sortCallback |
(items: T[]) => T[] |
default sort | Function to sort items (optional) |
| Prop | Type | Default | Description |
|---|---|---|---|
treeId |
string | null |
null |
Unique identifier for the tree |
parentPathMember |
string | null |
null |
Property name for parent path references |
levelMember |
string | null |
null |
Property name for node level |
isExpandedMember |
string | null |
null |
Property name for expanded state |
isSelectedMember |
string | null |
null |
Property name for selected state |
isDraggableMember |
string | null |
null |
Property name for draggable state |
isDropAllowedMember |
string | null |
null |
Property name for drop allowed state |
allowedDropPositionsMember |
string | null |
null |
Property name for allowed drop positions array |
hasChildrenMember |
string | null |
null |
Property name for children existence |
isSorted |
boolean | null |
null |
Whether items should be sorted |
| Prop | Type | Default | Description |
|---|---|---|---|
displayValueMember |
string | null |
null |
Property name for display text |
getDisplayValueCallback |
(node) => string |
undefined |
Function to get display value |
searchValueMember |
string | null |
null |
Property name for search indexing |
getSearchValueCallback |
(node) => string |
undefined |
Function to get search value |
shouldUseInternalSearchIndex |
boolean |
false |
Enable built-in search functionality |
initializeIndexCallback |
() => Index |
undefined |
Function to initialize search index |
searchText |
string (bindable) |
undefined |
Current search text |
Note: When shouldUseInternalSearchIndex is enabled, node indexing is performed asynchronously using requestIdleCallback (with fallback to setTimeout). This ensures the tree renders immediately while search indexing happens during browser idle time, providing better performance for large datasets.
Important: For internal search indexing to work, you must:
- Set
shouldUseInternalSearchIndex={true} - Provide either
searchValueMember(property name) orgetSearchValueCallback(function)
Without both requirements, no search indexing will occur.
Performance Tuning:
indexerBatchSizecontrols how many nodes are processed per idle callback. Lower values (10-25) provide smoother UI performance but slower indexing, while higher values (50-100) index faster but may cause brief UI pauses. Default: 25.indexerTimeoutsets the maximum wait time before forcing indexing when the browser is busy. Lower values (25-50ms) ensure more responsive indexing, while higher values (100-200ms) give more time for genuine idle periods. Default: 50ms.
| Prop | Type | Default | Description |
|---|---|---|---|
treeId |
string | null |
auto-generated | Unique identifier for the tree |
treePathSeparator |
string | null |
"." |
Separator character for hierarchical paths (e.g., "." for "1.2.3" or "/" for "1/2/3") |
selectedNode |
LTreeNode<T> (bindable) |
undefined |
Currently selected node |
insertResult |
InsertArrayResult<T> (bindable) |
undefined |
Result of the last data insertion including failed nodes |
| Prop | Type | Default | Description |
|---|---|---|---|
expandLevel |
number | null |
2 |
Automatically expand nodes up to this level |
shouldToggleOnNodeClick |
boolean |
true |
Toggle expansion on node click |
orderMember |
string | null |
null |
Property name for sort order (enables above/below positioning in drag-drop) |
indexerBatchSize |
number | null |
25 |
Number of nodes to process per batch during search indexing |
indexerTimeout |
number | null |
50 |
Maximum time (ms) to wait for idle callback before forcing indexing |
shouldDisplayDebugInformation |
boolean |
false |
Show debug information panel with tree statistics and enable console debug logging |
shouldDisplayContextMenuInDebugMode |
boolean |
false |
Display persistent context menu at fixed position for styling development |
| Prop | Type | Default | Description |
|---|---|---|---|
useFlatRendering |
boolean |
true |
Use flat rendering mode (faster for large trees) |
progressiveRender |
boolean |
true |
Progressively render nodes in batches |
initialBatchSize |
number |
20 |
First batch size for progressive rendering |
maxBatchSize |
number |
500 |
Maximum batch size cap |
| Prop | Type | Default | Description |
|---|---|---|---|
dragDropMode |
DragDropMode |
'none' |
Controls allowed drag operations: 'none', 'self', 'cross', 'both' |
dropZoneMode |
string |
'glow' |
Drop indicator style: 'floating' or 'glow' |
dropZoneLayout |
string |
'around' |
Zone arrangement: 'around', 'above', 'below', 'wave', 'wave2' |
dropZoneStart |
number | string |
33 |
Where zones start horizontally (number=%, string=CSS value) |
dropZoneMaxWidth |
number |
120 |
Max width in pixels for wave layouts |
allowCopy |
boolean |
false |
Enable Ctrl+drag to copy instead of move |
autoHandleCopy |
boolean |
true |
Auto-handle same-tree copies (false for external DB/API) |
allowedDropPositionsMember |
string | null |
null |
Property name for allowed drop positions array |
getAllowedDropPositionsCallback |
(node) => DropPosition[] | null |
undefined |
Callback returning allowed drop positions per node |
beforeDropCallback |
(dropNode, draggedNode, position, event, operation) => ... |
undefined |
Async-capable callback to validate/modify drops |
| Prop | Type | Default | Description |
|---|---|---|---|
onNodeClicked |
(node) => void |
undefined |
Node click event handler |
onNodeDragStart |
(node, event) => void |
undefined |
Drag start event handler |
onNodeDragOver |
(node, event) => void |
undefined |
Drag over event handler |
onNodeDrop |
(dropNode, draggedNode, position, event, operation) => void |
undefined |
Drop event handler. Position is 'above', 'below', or 'child'. Operation is 'move' or 'copy' |
| Prop | Type | Default | Description |
|---|---|---|---|
bodyClass |
string | null |
undefined |
CSS class for tree body |
selectedNodeClass |
string | null |
undefined |
CSS class for selected nodes |
dragOverNodeClass |
string | null |
undefined |
CSS class for nodes being dragged over |
expandIconClass |
string | null |
"ltree-icon-expand" |
CSS class for expand icons |
collapseIconClass |
string | null |
"ltree-icon-collapse" |
CSS class for collapse icons |
leafIconClass |
string | null |
"ltree-icon-leaf" |
CSS class for leaf node icons |
scrollHighlightTimeout |
number | null |
4000 |
Duration (ms) for scroll highlight animation |
scrollHighlightClass |
string | null |
'ltree-scroll-highlight' |
CSS class to apply for scroll highlight effect |
| Snippet | Parameters | Description |
|---|---|---|
nodeTemplate |
(node) |
Custom node template |
treeHeader |
Tree header content | |
treeBody |
Tree body content | |
treeFooter |
Tree footer content | |
noDataFound |
No data template | |
contextMenu |
(node, closeMenu) |
Context menu template |
| Method | Parameters | Description |
|---|---|---|
expandNodes |
nodePath: string |
Expand nodes at specified path |
collapseNodes |
nodePath: string |
Collapse nodes at specified path |
expandAll |
nodePath?: string |
Expand all nodes or nodes under path |
collapseAll |
nodePath?: string |
Collapse all nodes or nodes under path |
filterNodes |
searchText: string, searchOptions?: SearchOptions |
Filter the tree display using internal search index with optional FlexSearch options |
searchNodes |
searchText: string | null | undefined, searchOptions?: SearchOptions |
Search nodes using internal search index and return matching nodes with optional FlexSearch options |
scrollToPath |
path: string, options?: ScrollToPathOptions |
Scroll to and highlight a specific node |
update |
updates: Partial<Props> |
Programmatically update component props from external JavaScript |
addNode |
parentPath: string, data: T, pathSegment?: string |
Add a new node under the specified parent |
moveNode |
sourcePath: string, targetPath: string, position: 'above' | 'below' | 'child' |
Move a node to a new location |
removeNode |
path: string, includeDescendants?: boolean |
Remove a node (and optionally its descendants) |
getNodeByPath |
path: string |
Get a node by its path |
getChildren |
parentPath: string |
Get direct children of a node |
getSiblings |
path: string |
Get siblings of a node (including itself) |
| Option | Type | Default | Description |
|---|---|---|---|
expand |
boolean |
true |
Automatically expand parent nodes to make target visible |
highlight |
boolean |
true |
Apply temporary highlight animation to the target node |
scrollOptions |
ScrollIntoViewOptions |
{ behavior: 'smooth', block: 'center' } |
Native browser scroll options |
Usage Example:
// Basic usage - scroll to path with default options
await tree.scrollToPath('1.2.3');
// Advanced usage - custom options
await tree.scrollToPath('1.2.3', {
expand: false, // Don't auto-expand parent nodes
highlight: false, // Skip highlight animation
scrollOptions: { // Custom scroll behavior
behavior: 'instant',
block: 'start'
}
});Highlight Classes Example:
<!-- Default background highlight -->
<Tree
{data}
idMember="path"
pathMember="path"
scrollHighlightClass="ltree-scroll-highlight"
scrollHighlightTimeout={5000}
/>
<!-- Red arrow highlight -->
<Tree
{data}
idMember="path"
pathMember="path"
scrollHighlightClass="ltree-scroll-highlight-arrow"
scrollHighlightTimeout={3000}
/>
<!-- Custom highlight class -->
<Tree
{data}
idMember="path"
pathMember="path"
scrollHighlightClass="my-custom-highlight"
scrollHighlightTimeout={2000}
/>Available Built-in Highlight Classes:
ltree-scroll-highlight- Background glow with blue color (default)ltree-scroll-highlight-arrow- Red left arrow indicator
The tree provides real-time statistics about the loaded data:
| Property | Type | Description |
|---|---|---|
statistics |
{ nodeCount: number; maxLevel: number; filteredNodeCount: number; isIndexing: boolean; pendingIndexCount: number } |
Returns current node count, maximum depth level, filtered nodes count, indexing status, and pending index count |
const { nodeCount, maxLevel, filteredNodeCount, isIndexing, pendingIndexCount } = tree.statistics;
console.log(`Tree has ${nodeCount} nodes with maximum depth of ${maxLevel} levels`);
if (filteredNodeCount > 0) {
console.log(`Currently showing ${filteredNodeCount} filtered nodes`);
}
if (isIndexing) {
console.log(`Search indexing in progress: ${pendingIndexCount} nodes pending`);
}The update() method allows you to programmatically update component props from external JavaScript code (outside of Svelte's reactivity system). This is particularly useful for HTML/JavaScript integration or dynamic configuration from non-Svelte code.
// Get reference to the tree component
const treeElement = document.querySelector('#my-tree');
// Update multiple props at once
treeElement.update({
searchText: 'Production',
expandLevel: 3,
shouldDisplayDebugInformation: true,
data: newDataArray,
contextMenuXOffset: 10
});
// Update single prop
treeElement.update({ searchText: 'new search' });
// Update data and configuration
treeElement.update({
data: fetchedData,
expandLevel: 5,
selectedNodeClass: 'custom-selected'
});Updatable Properties: All Tree props can be updated except snippets/templates, including:
- Data and state:
data,searchText,selectedNode,expandLevel - Members:
idMember,pathMember,displayValueMember,searchValueMember - Callbacks:
sortCallback,getDisplayValueCallback,onNodeClicked, etc. - Visual:
bodyClass,selectedNodeClass,expandIconClass, etc. - Context menu:
contextMenuCallback,contextMenuXOffset,contextMenuYOffset - Behavior:
shouldToggleOnNodeClick,shouldUseInternalSearchIndex, etc.
Enable debug information to see real-time tree statistics and console logging:
<Tree
{data}
idMember="path"
pathMember="path"
shouldDisplayDebugInformation={true}
/>The visual debug panel shows:
- Tree ID
- Data array length
- Expand level setting
- Node count
- Maximum depth levels
- Filtered node count (when filtering is active)
- Search indexing progress (when indexing is active)
- Currently dragged node
When enabled, the component will log detailed information to the browser console including:
Tree Operations:
- Data mapping and sorting performance metrics
- Node filtering and search operations
- Tree structure changes
Async Search Indexing:
- Indexer initialization with batch size
- Queue management (items added, queue size)
- Batch processing details (timeout status, items processed, timing)
- Indexing completion and progress updates
This provides valuable insights for performance optimization and troubleshooting, especially when working with large datasets or complex search operations.
The component expects hierarchical data with path-based organization:
interface NodeData {
path: string; // e.g., "1.2.3" for hierarchical positioning
// ... your custom properties
}- Root level:
"1","2","3" - Second level:
"1.1","1.2","2.1" - Third level:
"1.1.1","1.2.1","2.1.1"
Important: For proper tree construction, your sortCallback must sort by level first to ensure parent nodes are inserted before their children:
const sortCallback = (items: LTreeNode<T>[]) => {
return items.sort((a, b) => {
// First, sort by level (shallower levels first)
const aLevel = a.path.split('.').length;
const bLevel = b.path.split('.').length;
if (aLevel !== bLevel) {
return aLevel - bLevel;
}
// Then sort by your custom criteria
return a.data.name.localeCompare(b.data.name);
});
};Why this matters: If deeper level nodes are processed before their parents, you'll get "Could not find parent node" errors during tree construction. Level-first sorting ensures hierarchical integrity and enables progressive rendering for large datasets.
The tree provides detailed information about data insertion through the insertResult bindable property:
interface InsertArrayResult<T> {
successful: number; // Number of nodes successfully inserted
failed: Array<{ // Nodes that failed to insert
node: LTreeNode<T>; // The processed tree node
originalData: T; // The original data object
error: string; // Error message (usually "Could not find parent...")
}>;
total: number; // Total number of nodes processed
}<script lang="ts">
import { Tree } from '@keenmate/svelte-treeview';
let insertResult = $state();
const data = [
{ id: '1', path: '1', name: 'Root' },
{ id: '1.2', path: '1.2', name: 'Child' }, // Missing parent "1.1"
{ id: '1.1.1', path: '1.1.1', name: 'Deep' } // Missing parent "1.1"
];
// Check results after tree processes data
$effect(() => {
if (insertResult) {
console.log(`${insertResult.successful} nodes inserted successfully`);
console.log(`${insertResult.failed.length} nodes failed to insert`);
insertResult.failed.forEach(failure => {
console.log(`Failed: ${failure.originalData.name} - ${failure.error}`);
});
}
});
</script>
<Tree
{data}
idMember="id"
pathMember="path"
displayValueMember="name"
bind:insertResult
/>- Data Validation: Identify missing parent nodes in hierarchical data
- Debugging: Clear error messages with node paths like "Node: 1.1.1 - Could not find parent node: 1.1"
- Data Integrity: Handle incomplete datasets gracefully
- Search Accuracy: Failed nodes are excluded from search index, ensuring search results match visible tree
- User Feedback: Inform users about data issues with detailed failure information
The component is optimized for large datasets:
- Flat Rendering Mode: Single
{#each}loop instead of recursive components (default, ~12x faster initial render) - Progressive Rendering: Batched rendering prevents UI freeze during initial load
- Context-Based Callbacks: Stable function references eliminate unnecessary re-renders
- LTree: Efficient hierarchical data structure
- Async Search Indexing: Uses
requestIdleCallbackfor non-blocking search index building - Accurate Search Results: Search index only includes successfully inserted nodes
- Search Indexing: Uses FlexSearch for fast search operations
| Operation | Time |
|---|---|
| Initial render | ~25ms |
| Expand/collapse | ~100-150ms |
| Search filtering | <50ms |
Flat Rendering Mode (default) - Renders all visible nodes in a single loop:
<Tree
{data}
useFlatRendering={true}
progressiveRender={true}
/>Optimized insertArray algorithm - Fixed O(n²) bottleneck. Now loads 17,000+ nodes in under 100ms.
Performance Logging - Built-in performance measurement for debugging:
import { enablePerfLogging } from '@keenmate/svelte-treeview';
enablePerfLogging();
// Or from browser console:
window.components['svelte-treeview'].perf.enable()Important: See the $state.raw() tip above - using $state() instead of $state.raw() for tree data can cause 5,000x slowdown!
For developers working on the project, you can use either standard npm commands or the provided Makefile:
# Using Makefile (recommended for consistency)
make setup # or make install
make dev
# Or using standard npm commands
npm install
npm run devWe welcome contributions! Please see our contributing guidelines for details.
For AI Agents / LLMs: Comprehensive documentation is available in the
ai/folder with topic-specific files (basic-setup.txt, drag-drop.txt, performance.txt, etc.). Start withai/INDEX.txtfor navigation.
MIT License - see LICENSE file for details.
- GitHub Issues: Report bugs or request features
- Live demo & docs: svelte-treeview.keenmate.dev
Built with ❤️ by KeenMate