npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dineug/erd-editor

v5.1.0

Published

Entity-Relationship Diagram Editor

Readme

@dineug/erd-editor

Entity-Relationship Diagram Editor as a custom element

ERD Editor

<erd-editor> is a database-schema editor in one custom element. It has no framework dependency and renders into a closed shadow root, so it drops into React, Vue, Svelte or plain HTML without leaking styles either way. It is the same editor as erd-editor.io, the VS Code extension, the JetBrains plugin and the Obsidian plugin. Beside it, <erd-viewer> shows a diagram read-only, for a page that only displays one.

Features

  • Design — tables, columns, memos and table groups, four relationship cardinalities, ON DELETE and ON UPDATE, alternate keys, Auto Layout
  • Naming standards — logical and physical names, physical names and data types derived from word, term and domain dictionaries, which have their own tab with CSV import and export and checks
  • Import — SQL DDL, GraphQL SDL, DBML and AML from the menu or setSchema*, replacing the diagram or adding to it
  • Export — DDL for eight databases, also from getSchemaSQL(), code in 22 languages and ORMs, PNG and SVG, also from toImage()
  • Navigate — command palette (Ctrl/Cmd+K), an Explorer tree with find and replace, Graph and Flow views
  • History — undo and redo, time travel through the session, a diff viewer (setDiffValue())
  • 25 languages and a System, Light or Dark theme, their pickers opt-in with an attribute
  • Collaboration — getSharedStore() relays edits over your own transport, and the headless entries run the same document with no DOM

Databases: Databricks, MariaDB, MSSQL, MySQL, Oracle, PostgreSQL, Snowflake, SQLite

Code: TypeScript, GraphQL, C#, Java, JPA, Kotlin, Scala, Go, SQLAlchemy, TypeORM, Sequelize, Drizzle, DBML, AML, Mermaid, PHP, Doctrine, Rust, SeaORM, Swift, Zod, JSON Schema

Install

npm install @dineug/erd-editor

The editor's PNG and SVG export, the Flow and tree layouts and the syntax highlighting run in shared workers, which Vite, webpack and Rspack emit beside your chunks and esbuild does not. webpack's experimental experiments.outputModule starts none of them, as each waits on a chunk split off it that nothing loads: keep webpack's default output, or set optimization.splitChunks: false. A strict CSP needs worker-src 'self'. <erd-viewer> starts no worker. Both elements style themselves with constructed stylesheets, which style-src 'self' allows, but the editor's code panels color their text with style attributes and its image export previews from a blob: url, so they need style-src 'unsafe-inline' and img-src blob:. A browser without constructed stylesheets, as Firefox before 101, gets <style> elements, which need style-src 'unsafe-inline' too. Without a bundler, use the script tag builds.

Usage

import '@dineug/erd-editor/erd-editor.js';

const editor = document.createElement('erd-editor');
// a custom element is inline, and the editor fills its box
Object.assign(editor.style, { display: 'block', width: '100%', height: '100vh' });
document.body.appendChild(editor);

editor.setInitialValue(localStorage.getItem('my-diagram') ?? '');
editor.addEventListener('change', () => {
  localStorage.setItem('my-diagram', editor.value);
});

Registering the elements

@dineug/erd-editor/erd-editor.js defines <erd-editor> when imported, and @dineug/erd-editor/erd-viewer.js defines <erd-viewer>. Importing the package root defines neither. It exports both element classes, ErdEditorElement and ErdViewerElement, and defineErdEditor() and defineErdViewer(), which define their tag where you call them:

import { defineErdEditor } from '@dineug/erd-editor';

defineErdEditor();

A second call does nothing, and a call throws an Error naming the tag when another class already holds it.

A bundle that holds both elements makes Vite and Rolldown warn 24 times of an INEFFECTIVE_DYNAMIC_IMPORT: the viewer loads each language but English with import(), and the editor imports every dictionary at once, so they stay in the editor's chunk and load with it.

For a tag of your own, define the class yourself and add the tag to HTMLElementTagNameMap yourself, in a window that loads neither that element's entry nor its script tag build and calls no define function for it, since a class takes one name per window:

import { ErdEditorElement } from '@dineug/erd-editor';

customElements.define('my-erd-editor', ErdEditorElement);

declare global {
  interface HTMLElementTagNameMap {
    'my-erd-editor': ErdEditorElement;
  }
}

Migrating from the bare import

Importing the package root used to define <erd-editor>. It now defines nothing, so an element created after that import alone is a plain HTMLElement without the editor's methods. Import the entry for its effect instead, or call defineErdEditor():

-import '@dineug/erd-editor';
+import '@dineug/erd-editor/erd-editor.js';
 import { setExportFileCallback } from '@dineug/erd-editor';

Server-side rendering

Every entry but engine.js and peer.js needs a DOM, the package root too, since evaluating it builds both element classes. So in Next.js, Nuxt, SvelteKit or Astro import the element on the client:

useEffect(() => {
  import('@dineug/erd-editor/erd-editor.js');
}, []);

Vite

In development, include the ELK bundle the editor's layout worker imports in Vite's pre-bundling, or the Flow and tree layouts fail. Vite 7 and older also need the editor kept out of pre-bundling, since their pre-bundle points the workers' URLs at files it never writes. A production build needs nothing.

// vite.config.js
export default {
  optimizeDeps: {
    include: ['@dineug/erd-editor > elkjs/lib/elk.bundled.js'],
    // Vite 7 and older only:
    // exclude: ['@dineug/erd-editor'],
  },
};

Script tag

Two builds bundle every dependency, so a page loads one with no bundler. Pin a version beyond a demo.

  • dist/erd-editor.umd.js bundles the workers too, defines <erd-editor> and puts setExportFileCallback, setImportFileCallback, createKeyBindingMap, dictionaryRowChords, ErdEditorElement and defineErdEditor on window.ErdEditor. Its workers are data: URLs, so a strict CSP needs worker-src data:.
  • dist/erd-viewer.umd.js defines <erd-viewer> and puts ErdViewerElement and defineErdViewer on window.ErdViewer. It starts no worker and holds all 25 languages.

The package's unpkg and jsdelivr fields name the editor's file, so a CDN URL without a path serves the editor.

<erd-editor style="display: block; width: 100%; height: 100vh"></erd-editor>
<script src="https://cdn.jsdelivr.net/npm/@dineug/erd-editor/dist/erd-editor.umd.js"></script>
<erd-viewer src="diagram.erd.json" style="display: block; height: 480px"></erd-viewer>
<script src="https://cdn.jsdelivr.net/npm/@dineug/erd-editor/dist/erd-viewer.umd.js"></script>

API

ErdEditorElement is exported as both the element's type and its class, beside defineErdEditor() (Registering the elements). The Element API docs cover each member in full. The read-only element has a section of its own, Viewer.

Attributes

| Attribute | Property | Description | | --- | --- | --- | | readonly | readonly | Blocks editing and change; only setInitialValue() still loads. Takes the edit UI away and keeps navigation, export, the diff viewer and Table Properties to read; a setting the reader changes stays on their own screen. | | system-dark-mode | systemDarkMode | Sets the appearance to system. | | enable-theme-builder | enableThemeBuilder | Shows the theme builder and the palette's Theme. | | enable-locale-picker | enableLocalePicker | Shows the language button and the palette's Display Language, System by default. | | enable-welcome-screen | enableWelcomeScreen | Shows the welcome screen over an empty, editable diagram. |

An attribute is on when present with no value or "true".

Properties

| Property | Description | | --- | --- | | value: string | The document as a file saves it (schema). Assigning one, or '' for a new one, is an undoable load. | | runtimeValue: string | Read only. The document as this editor holds it, to seed a collaborator. |

Methods

| Method | Description | | --- | --- | | setInitialValue(value) | Load a document with no undo entry, ending a relationship draw and closing a memo, group name or cell editor left open, where a logical name cell the reader typed in derives its name as any close does; '' starts a new one. | | getSchemaSQL(vendor?, options?) | DDL for one of the eight databases, by default the one this editor shows (the document's, unless a readonly reader picked another), always in the bracket it shows. options: statements (create, ifNotExists, recreate), header (none, use, createAndUse), logicalNameAsComment (true writes a table's or column's logical name where its comment is empty). | | setSchemaSQL(value, options?), setSchemaGraphQL, setSchemaDBML, setSchemaAML | Replace the document with parsed DDL, GraphQL SDL, DBML or AML, or append it (Importing a schema). | | setSchemaJSON(value, options?) | Load an .erd.json document as value does, or append it. | | setDiffValue(value) | Diff another document over the ERD tab. | | setPresetTheme(options) | Set appearance (light, dark, system), grayColor and accentColor. | | setSystemAppearance(appearance) | Name the light or dark system shows; null follows the OS. | | setLocale(locale) | Set the display language, 'system' or a code. | | setSystemLocale(tag) | Name the BCP 47 language system shows; null reads navigator.languages. | | setTheme(theme) | Override theme tokens; each call replaces the earlier overrides. | | setKeyBindingMap(map) | Remap shortcuts but edit, stop, undo, redo and the zoom keys. | | getSharedStore(config?) | A shared store for collaboration. | | focus() / blur() | Move focus in or out. | | clear() | Empty the document and keep its settings, in one undo step. | | destroy() | Release the editor's listeners, subscriptions and shared stores. |

Events

| Event | Description | | --- | --- | | change | The document changed; read editor.value. | | changePresetTheme | The theme changed in the editor; event.detail holds the options. | | changeLocale | A language was picked in the editor; event.detail.locale holds it. |

Loading and saving

  • Save value. runtimeValue also keeps removed entities and only seeds a collaborator.
  • change fires 200 ms after the last change, never under readonly, and never for scroll, zoom, the open tab, the name view or the code settings while the document locks them, which a new document does.
  • A file written by hand or by another release can differ from value once loaded, so detect edits with a replica's changed, not by comparing bytes.

Importing a schema

  • The four text setSchema* methods take { placement?: 'auto' | 'grid', mode?: 'replace' | 'append' }, and setSchemaJSON only mode. Each lands in one undo step and ignores ''.
  • 'grid', the default, lands at once in rows. 'auto' lays the tables out as the Import menu does and returns a Promise that resolves when they land; a later load supersedes it.
  • mode: 'append' adds the tables below the diagram, as Import and Add does.
  • A replace by the four text methods keeps the document's word, term and domain dictionaries, which the text cannot hold; a replacing setSchemaJSON takes the ones its document holds, as value does.

Theme and language

The element starts dark and English and stores neither choice: setPresetTheme and setLocale set them, and your host keeps what changePresetTheme and changeLocale report.

Viewer

<erd-viewer> shows a document read-only: a reader pans, zooms, searches and lights relationships, and nothing in it edits. It renders into a closed shadow root as the editor does, and several on one page each keep their own document and view.

import '@dineug/erd-editor/erd-viewer.js';
<erd-viewer src="/diagrams/shop.erd.json" style="display: block; height: 480px"></erd-viewer>

A page that cannot load the element can frame the hosted viewer, https://erd-editor.io/embed?src=…, instead; the hosted viewer also takes a file or gist on GitHub by the address its page shows, from public repositories and public gists only, fetched from GitHub's raw host with its five-minute cache.

Viewer attributes and properties

| Attribute | Property | Description | | --- | --- | --- | | src | src | The URL of a document as an .erd.json file saves it, in no other format, fetched with GET under the browser's CORS rules. | | name-view | nameView | physical, logical or both, shown over the name view the document saves, which value keeps. | | minimap | minimap | The minimap, which shows while the document has content. | | controls | controls | The controls bar: zoom out, the zoom level, which resets it, zoom in, Fit, the name view list, and an arrow to the nearest part of the diagram while none of it is on screen. |

minimap and controls are on unless their value is "false".

| Property | Description | | --- | --- | | value: string | The document as an .erd.json file saves it, as the editor's value does: while the document locks its viewport, which a new document does, it keeps the zoom and scroll the file saved, and only with that lock off holds the reader's. Assigning one shows it and throws on text that does not parse; '' shows an empty diagram. |

Whichever of value and src is set last wins, so assigning value cancels a fetch still running. A value, src, nameView, minimap or controls set on the element before its tag is defined is taken up once it is, over the attributes its markup gave it.

Viewer methods

| Method | Description | | --- | --- | | setPresetTheme(options) | Set appearance (light, dark, system), grayColor and accentColor. | | setSystemAppearance(appearance) | Name the light or dark system shows; null follows the OS. | | setTheme(theme) | Override theme tokens; each call replaces the earlier overrides. | | setLocale(locale) | Set the display language, 'system' or a code. The Promise it returns resolves once that language's dictionary has loaded or failed to. | | setSystemLocale(tag) | Name the BCP 47 language system shows; null goes back to the page's languages. It resolves as setLocale does. | | destroy() | Take the view down, abort a fetch and stop watching the OS color scheme and the page's styles. A destroyed viewer draws nothing more, even when added to the page again. |

Viewer events

| Event | Description | | --- | --- | | load | The document src names was fetched and shown. Assigning value fires none. | | error | Fetching src, its response or its document failed; event.detail holds the cause. The viewer shows an empty diagram and a notice. |

Loading, focus and language

  • Loading. A notice over the canvas shows while src loads and once it failed. A new src aborts the fetch before it, removing the viewer from the page aborts the one running, and a viewer put back fetches a src it has not shown. Each document is fitted to the viewer's box as it lands, the zoom and scroll the file saved left aside.
  • Focus. The viewer takes no focus as it mounts. Until a click, a tap or the Tab key focuses it, the wheel and a swipe from empty canvas scroll the page, a trackpad pinch passes to the page, a finger pans nothing, and it answers no key. Once focused, the wheel pans it (Shift across) and so does a finger, Ctrl/Cmd with the wheel or a pinch zooms, Ctrl/Cmd+=, - and 0 zoom in, out and back, and Ctrl/Cmd+K opens a palette that searches the document: # tables, @ columns and : comments and memos. A mouse drag pans and a still click or tap lights relationships, focused or not: a table's, or the one clicked; the table the pointer rests on lights its own.
  • Theme and language. Both start on system: the OS color scheme, and the page's <html lang>, then navigator.languages, read once as the viewer first connects. English is in the entry. Each other language is a chunk of its own, which your bundler emits and the viewer loads once a page asks for it, showing English until it lands and keeping English if it fails. The palette and the notices follow a right-to-left language; the canvas and the controls stay left to right.

What it adds to a page

  • src is the one fetch the package makes, so a strict CSP lists its origin in connect-src. Besides it, a viewer requests only the dictionary chunk of a language other than English, from your own bundle. Its styles are constructed stylesheets, which a strict CSP lets through, but for a browser without them, which gets <style> elements.
  • It starts no worker and writes no storage. Importing it adds three listeners to window for the life of the page, for mousedown, touchstart and an event of the package's own, @dineug/erd-editor/internal-forward-move-start, and Konva, which draws the canvas, sets window.Konva and, with its first stage, adds mouse and touch listeners to window and a visibilitychange listener to document, which it never removes. The editor's entry adds the same.

Collaboration

A joiner starts from the first editor's runtimeValue, then each relays its actions:

joiner.setInitialValue(editor.runtimeValue);

const local = editor.getSharedStore();
const remote = joiner.getSharedStore();
local.subscribe(actions => remote.dispatch(actions));
remote.subscribe(actions => local.dispatch(actions));

An editor never forwards what it received, so with three or more, send each one's batches to every other. Call flushStreamBuffers() before destroying a store, or the last drags and resizes never reach the peers. getSharedStore({ getNickname, mouseTracker, focusTracker }) also shares cursors and focus. To share a diagram through a Yjs document and any of its providers instead, @dineug/erd-editor-yjs binds the element and the headless peer store to a Y.Doc.

| Method | Description | | --- | --- | | subscribe(fn) | Take this editor's batches to send to the peers. The first subscriber also asks them for their registers. | | dispatch(actions), dispatchSync(actions) | Apply a peer's batch, in a microtask or at once. | | connection(), disconnect() | Resume or hold sending; what this editor does meanwhile waits. | | flushStreamBuffers() | Send the drags, pans, resizes and color strokes still held back. | | getLWW() | A JSON copy of the editor's registers, to hand over beside runtimeValue as it is. | | mergeLWW(lww, version) | Take another store's registers and version, sending nothing. | | getVersion() | The highest version the editor's clock has merged. | | forgetPeer(editorId) | Drop the cursor, focus, selection and marquee a peer that left had sent. | | reseed(value, { lww, version, keepScreen }) | Open a session's document again, as a rejoin does: no undo entry, no change event and nothing sent but the request for registers and, with focusTracker, the selection and focus it clears, save the name a logical name cell the reader typed in derives as it closes, as any close does. The undo history, the selection, the focus, an open Flow view and a relationship draw or an editor left open go. keepScreen keeps what the reader sees while value reports the document's: the scroll and zoom, the tab, the name view and the code settings while the document locked them before the reseed and still does at the locked value the reader last saw, and the settings a readonly reader changed on their own screen. An editor never shown, or not shown since its last load, opens where the value puts it. Drags and color strokes still held back go out after a reseed, so call flushStreamBuffers() first and reseed with a value that holds what it sent, and never reseed while disconnect() holds batches. | | destroy() | Release the store. |

Key bindings

createKeyBindingMap() returns the default shortcuts, and setKeyBindingMap replaces each list it names. A chord is modifiers (Shift, Alt, Control, Meta, or $mod for Cmd on macOS and Ctrl elsewhere) joined by + with a KeyboardEvent.code or key; a space separates the chords of a sequence, such as KeyT KeyG. Several defaults, search and find and replace among them, stop the keydown, so to leave $mod+KeyF to the page, call setKeyBindingMap({ findReplace: [] }). dictionaryRowChords(map) lists the chords the Dictionary tab's grid inserts and deletes rows by under a map: the one-press chords of addColumn and removeColumn, and those of addColumn with Shift held as well, for Insert above, which no binding names. A host that keeps its own hotkeys off the editor's keys takes both lists.

Display language

en, ar-SA, de-DE, es-ES, eu-ES, fa-IR, fr-FR, he-IL, id-ID, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, ru-RU, sk-SK, sl-SI, sv-SE, tr-TR, uk-UA, zh-CN and zh-TW. Arabic, Hebrew and Persian lay the editor out right to left. The LocaleCode and LocaleOption types are exported.

Workers

Shiki highlights the code panels with a JavaScript regex engine, so a CSP needs no wasm-unsafe-eval. Without SharedWorker, as in Chrome on Android before Chrome 148, the code panels show plain text, the Flow view and the Flow and tree layouts place nothing, an 'auto' import lands in rows, and PNG and SVG exports draw on the main thread.

File dialogs

Import and export go through page-wide callbacks, so a host without a browser file dialog, such as an IDE webview, supplies its own:

import { setExportFileCallback, setImportFileCallback } from '@dineug/erd-editor';

setExportFileCallback((blob, { fileName }) => host.writeFile(fileName, blob));

setImportFileCallback(async ({ type, op, accept, mode }) => {
  const text = await host.pickFile(accept);
  if (op === 'diff') return editor.setDiffValue(text);
  if (type === 'json') return editor.setSchemaJSON(text, { mode });
  const format = { sql: 'SQL', graphql: 'GraphQL', dbml: 'DBML', aml: 'AML' }[type];
  if (format) editor[`setSchema${format}`](text, { placement: 'auto', mode });
});

Skip a type you do not handle: a replacing setSchemaJSON() empties the document on any other payload. Unset or null, the browser's download and file picker work. The Dictionary tab's Import CSV always opens the browser's file picker, since no type names CSV, and its Export CSV goes through the export callback.

Images

toImage() draws a document, the text a value holds, as a PNG Blob or an SVG string, with the Export image dialog's options. It draws the whole document at 100%, whatever zoom it was saved at, and needs a browser: it measures and draws on a canvas, in a SharedWorker where there is one.

import { toImage } from '@dineug/erd-editor';

const png = await toImage(value, { format: 'png', scale: 3 });
const svg = await toImage(value, { format: 'svg', darkMode: true, background: false });

| Option | Default | Description | | --- | --- | --- | | format | | 'png' resolves to a Blob, 'svg' to a string. | | background | true | Paint the canvas color behind the diagram; false leaves it transparent. | | darkMode | false | Draw the dark theme in place of the light one. | | scale | 2 | 1, 2 or 3 image pixels per unit, PNG only; cut to what a canvas holds. |

It rejects text that does not parse, as the viewer's value does, and a value outside the table, and reads no other option. Empty text, or JSON with none of a document's fields, draws an empty diagram. The words the diagram draws itself, such as an unnamed table's placeholder, are English.

Headless replica

engine.js applies the same actions with no DOM, so a worker can keep and serialize the document:

import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const store = createReplicationStore({ toWidth }); // a worker has no DOM to measure text
store.setInitialValue(savedJson);
store.on({ change: ({ value, changed }) => changed && persist(value) });
store.dispatch(actions);

changed is false when value matches what the store last reported or loaded.

peer.js runs a collaboration peer with no DOM: createPeerStore({ nickname }) dispatches actions, receives a peer's batches and undoes its own. It has the shared store's getLWW, mergeLWW, getVersion, forgetPeer and reseed too, where keepScreen changes nothing. The ERD Editor MCP server is built on it.

Development

pnpm --filter @dineug/erd-editor dev            # dev server, workspace deps built first
pnpm --filter @dineug/erd-editor dev:storybook  # component playground
pnpm exec vp run --filter @dineug/erd-editor --fail-if-no-match test
pnpm --filter @dineug/erd-editor e2e            # Playwright

What a change runs and carries is in AGENTS.md.

Documentation

Issues

Found a bug or want a feature? Open an issue.

License

MIT © SeungHwan-Lee