Canvas 2D rendering plugin for @keenmate/svelte-treeview. Renders trees on an HTML Canvas with pan, zoom, multiple layout modes, and full interactivity.
This package was originally part of @keenmate/svelte-treeview but has been extracted into its own package to separate concerns — the core package handles tree data, expand/collapse, search, drag & drop logic, while this package provides a high-performance canvas renderer on top of the same core. You still need @keenmate/svelte-treeview installed — it is a required peer dependency.
ClickBehavior type shared with core: ClickBehavior ('select' | 'expand' | 'expand-and-focus') is now imported from @keenmate/svelte-treeview — single source of truth for both packages.navigationOverrides prop for custom overrides.autoFocusOnSelect: Auto-pans canvas viewport to the selected node and scrolls the page to the canvas if needed.enableClipboard prop for Ctrl+C/X/V support. Cut nodes rendered at 40% opacity.onNodeContextMenu → getNodeContextMenuItemsCallback (and similar) for clearer event vs data-provider distinction.onCanvasContextMenu prop: Right-click empty canvas space (no node) for a canvas-level context menu. Returns ContextMenuEntry[], renders the same styled menu without a node header.ContextMenuEntry / ContextMenuDivider / ContextMenuItem types from @keenmate/svelte-treeview. Full support for icons, keyboard shortcuts, nested submenus, named dividers, isVisible, isDisabled, className="danger", and async onclick.V, Ctrl+C) triggers the matching item. Escape closes the menu./examples/context-menu with 7 presets showcasing all menu features, plus canvas-level menu demo.npm install @keenmate/svelte-treeview @keenmate/svelte-treeview-canvas
Both packages are required. @keenmate/svelte-treeview provides the core tree logic; this package provides the canvas renderer.
<script lang="ts">
import { CanvasTree } from '@keenmate/svelte-treeview-canvas';
import type { LTreeNode } from '@keenmate/svelte-treeview';
const data = [
{ id: 1, path: '1', parentPath: '', name: 'Root', hasChildren: true },
{ id: 2, path: '1.1', parentPath: '1', name: 'Child A', hasChildren: false },
{ id: 3, path: '1.2', parentPath: '1', name: 'Child B', hasChildren: false },
];
function sortByName(items: LTreeNode<any>[]) {
return [...items].sort((a, b) => (a.data?.name || '').localeCompare(b.data?.name || ''));
}
</script>
<div style="height: 500px;">
<CanvasTree
{data}
idMember="id"
pathMember="path"
sortCallback={sortByName}
isSorted={true}
expandLevel={3}
getNodeLabelCallback={(node) => node.data?.name || node.path}
/>
</div>
--base-* shared design tokens flow into --ct-* component variables. Priority chain: theme prop > --ct-* > --base-* > hardcoded defaultfocusOnPath() / focusOnNode() with anchor, zoom, and animation optionslevelConfig| Mode | Description |
|---|---|
tree |
Classic hierarchical dendrogram |
balanced |
Balanced horizontal tree |
fishbone |
Alternating top/bottom branches |
radial |
Radial/sunburst circular layout |
box |
Grid-based box layout |
sunburst |
Accordion sunburst with arc rendering |
<CanvasTree {data} layoutMode="fishbone" growthDirection="right" ... />
Set shared design tokens on any ancestor element:
<div style="--base-accent-color: #e11d48; --base-main-bg: #fff1f2;">
<CanvasTree {data} ... />
</div>
Or use the theme prop for direct overrides:
<CanvasTree {data} theme={{ bg: '#1a1a2e', nodeText: '#e2e8f0' }} ... />
Key CSS variables: --base-accent-color, --base-main-bg, --base-elevated-bg, --base-text-color-1, --base-border-color, --base-font-family. Component-specific: --ct-node-radius, --ct-conn-color, --ct-menu-bg, etc.
import type { LTreeNode, ContextMenuEntry } from '@keenmate/svelte-treeview';
function getContextMenu(node: LTreeNode<Item>): ContextMenuEntry[] {
return [
{ icon: '👤', label: 'View', shortcut: 'V', onclick: () => view(node) },
{ divider: true, label: 'Actions' },
{ icon: '✏️', label: 'Edit', isDisabled: node.data.readonly, onclick: () => edit(node) },
{ icon: '📋', label: 'Copy...', children: [
{ label: 'Name', onclick: () => copyName(node) },
{ label: 'Email', onclick: () => copyEmail(node) },
]},
{ divider: true, label: 'Danger zone' },
{ icon: '🗑️', label: 'Delete', className: 'danger', onclick: () => del(node) },
];
}
<CanvasTree {data} onNodeContextMenu={getContextMenu} ... />
Override individual render slots or the entire node:
import type { CanvasRenderContext } from '@keenmate/svelte-treeview-canvas';
function renderBody(rctx: CanvasRenderContext<Item>) {
const { ctx, node, bounds, config, theme } = rctx;
ctx.font = config.fontBold;
ctx.fillStyle = theme.nodeText;
ctx.fillText(node.data?.name || '', bounds.x + 10, bounds.y + bounds.h / 2);
}
<CanvasTree {data} renderBodyCallback={renderBody} ... />
Run the dev server to browse interactive examples:
npm run dev
MIT - KeenMate