@emkodev/svg-engine
v0.2.0
Published
Declarative SVG graph canvas — web components for interactive node/edge diagrams
Maintainers
Readme
@emkodev/svg-engine
Declarative SVG graph canvas as native web components. Drop in <svg-canvas> with <svg-node> and <svg-edge> children and get a fully interactive diagram — pan, zoom, drag, connect — with zero JavaScript required from the consumer.
Install
npm install @emkodev/svg-engineUsage
HTML (script tag)
<svg-canvas>
<svg-node id="a" x="200" y="200" title="Start" start></svg-node>
<svg-node id="b" x="400" y="300" title="End"></svg-node>
<svg-edge from="a" to="b"></svg-edge>
</svg-canvas>
<script type="module" src="node_modules/@emkodev/svg-engine/dist/index.js"></script>ES module
import '@emkodev/svg-engine';<svg-canvas>
<svg-node id="a" x="200" y="200" title="Start" start></svg-node>
<svg-node id="b" x="400" y="300" title="End"></svg-node>
<svg-edge from="a" to="b"></svg-edge>
</svg-canvas>Elements
<svg-canvas>
The root container. Renders an infinite scrollable SVG world with a dot-grid background, edge layer, and node layer inside its shadow DOM. Provides a default toolbar and panel that activate automatically — no configuration needed.
| Attribute | Default | Description |
|----------------|---------|------------------------------------|
| world-width | 6000 | SVG canvas width in world units |
| world-height | 4000 | SVG canvas height in world units |
Slots
| Slot | Description |
|-------------|--------------------------------------------------------------|
| toolbar | Replace the default toolbar. Omit to use the built-in one. |
| panel | Replace the default node-edit panel. Omit for the built-in. |
| (default) | <svg-node> and <svg-edge> declarations, plus any other light DOM content. |
Declarative wiring
Any element inside the canvas (including slotted toolbar/panel) can trigger canvas actions with data-action and sync node attributes with data-node-attr — no JavaScript needed:
<svg-canvas>
<div slot="toolbar">
<button data-action="create-node">+ Node</button>
<button data-action="zoom-in">+</button>
<button data-action="zoom-out">−</button>
<button data-action="fit">Fit</button>
<span data-zoom-display></span>
</div>
<div slot="panel" popover="manual">
<input type="text" data-node-attr="title" placeholder="Title">
<label><input type="checkbox" data-node-attr="start"> Start</label>
<button data-action="delete-node">Delete</button>
</div>
</svg-canvas>data-action values: create-node, zoom-in, zoom-out, fit, delete-node.
data-node-attr value: any attribute name on <svg-node> (title, start, incomplete, …).
Public API
canvas.createNode(x?: number, y?: number): void
canvas.deleteSelected(): void // also bound to Delete/Backspace keys
canvas.zoomIn(): void
canvas.zoomOut(): void
canvas.zoomBy(delta: number): void
canvas.applyZoom(z: number): void
canvas.fitToContent(): void
canvas.getZoom(): number
canvas.worldCoords(clientX: number, clientY: number): { x: number; y: number }
canvas.refreshEdges(): void
canvas.refreshEdgesForNode(id: string): void
canvas.refreshOrphans(): voidEvents
All events bubble.
| Event | detail | Fired when |
|------------------|-----------------------------|------------------------------------------|
| zoom-change | { zoom: number } | Zoom level changes |
| node-select | { id: string } | A node is selected |
| node-deselect | — | Selection is cleared |
| node-move | { id: string } | A node is dropped after dragging |
| node-delete | { id: string } | A node is deleted |
| edge-create | { from: string, to: string } | A new edge is drawn between ports |
| edge-click | { from: string, to: string } | An edge is clicked (also removes it) |
| canvas-dblclick| { x: number, y: number } | Canvas double-clicked (cancelable — cancel to suppress node creation) |
canvas.addEventListener('node-move', (e: NodeMoveEvent) => {
console.log('moved', e.detail.id);
});Keyboard shortcuts
| Key | Action |
|-----------------|-------------------------|
| Shift+F | Fit content to viewport |
| + / = | Zoom in |
| - / _ | Zoom out |
| Arrow keys | Scroll |
| Delete / ⌫ | Delete selected node |
| Escape | Deselect |
<svg-node>
Represents a graph node. Must be a direct child of <svg-canvas>.
| Attribute | Type | Description |
|--------------|---------|--------------------------------------|
| id | string | Required. Unique node identifier |
| x | number | World x position |
| y | number | World y position |
| title | string | Label text (truncated at 22 chars) |
| start | boolean | Marks the node as a start node (shows accent sidebar) |
| incomplete | boolean | Shows an orange indicator dot |
<svg-edge>
Represents a directed edge between two nodes. Must be a direct child of <svg-canvas>.
| Attribute | Type | Description |
|-----------|--------|-------------------------------|
| from | string | Source node id |
| to | string | Target node id |
Theming
The canvas exposes CSS custom properties. All have typed @property declarations with light-dark() initial values, so the component adapts to the OS color scheme out of the box when no overrides are set.
| Property | Default (dark) | Default (light) |
|-----------------|-------------------------------|-------------------------------|
| --accent | #5b6af0 | #5b6af0 |
| --bg | #0f0f0f | #f4f4f5 |
| --surface | #1a1a1a | #ffffff |
| --border | oklch(1 0 0 / .08) | oklch(0 0 0 / .1) |
| --border-hover| oklch(1 0 0 / .2) | oklch(0 0 0 / .25) |
| --text | #e8e6e0 | #111111 |
| --text-muted | #666666 | #555555 |
Override in CSS:
svg-canvas {
--accent: #e040fb;
--bg: #0a0a0a;
}TypeScript types
import type {
ZoomChangeEvent,
NodeSelectEvent,
NodeMoveEvent,
NodeDeleteEvent,
EdgeCreateEvent,
CanvasDblClickEvent,
} from '@emkodev/svg-engine';License
MIT
