Install
npm install @dineug/erd-editor
The package is ESM-only ("type": "module") and ships only its dist folder.
There is no CommonJS build, so require('@dineug/erd-editor') does not work.
Its runtime dependencies are left external, as bare imports your bundler resolves, dedupes, and tree-shakes.
Its shared workers are emitted as separate entry files under dist/workers/ and constructed with new URL('./…', import.meta.url), the spelling Vite, webpack 5, and Rspack bundle as a worker entry. See Web Workers.
For a page with no bundler behind it there is a second, self-contained build — see Script tag.
Usage
import '@dineug/erd-editor';
const editor = document.createElement('erd-editor');
editor.style.cssText = 'display: block; width: 100%; height: 100vh;';
document.body.appendChild(editor);
// load a document without adding an undo entry, then keep it in sync
editor.setInitialValue(localStorage.getItem('my-diagram') ?? '');
editor.addEventListener('change', () => {
localStorage.setItem('my-diagram', editor.value);
});
<erd-editor> has no intrinsic size. Give it (or its container) an explicit width and height.
setInitialValue('') starts an empty document. Assigning value loads one as an edit instead, so it lands in the undo history.
See ErdEditorElement for the rest of the API.
CDN
<script type="module">
import 'https://esm.run/@dineug/erd-editor';
const editor = document.createElement('erd-editor');
editor.style.cssText = 'display: block; width: 100%; height: 100vh;';
document.body.appendChild(editor);
</script>
<!-- or -->
<script type="module" src="https://esm.run/@dineug/erd-editor"></script>
esm.run resolves the package's external dependencies for you, so this works without a bundler.
The unversioned URL always serves the latest release. Pin a version — https://esm.run/@dineug/[email protected] — if you do not want a major upgrade to reach your page unannounced.
Script tag
Since 3.6.0 the package also ships a UMD build with every dependency and all four shared workers inside one file.
It is what the unpkg and jsdelivr fields point at, so the bare package URL on either CDN serves it, and it defines window.ErdEditor.
<erd-editor></erd-editor>
<script>
const editor = document.querySelector('erd-editor');
editor.setInitialValue(localStorage.getItem('my-diagram') ?? '');
</script>
Importing it registers <erd-editor> the same way; window.ErdEditor carries the two file callbacks, ErdEditor.setExportFileCallback and ErdEditor.setImportFileCallback.
The exports map still points a bundler at the ES modules, so an install from npm never picks this file up.
HTML
<erd-editor system-dark-mode enable-theme-builder></erd-editor>
<script type="module">
import 'https://esm.run/@dineug/erd-editor';
const editor = document.querySelector('erd-editor');
</script>
erd-editor {
display: block;
width: 100%;
height: 100vh;
}
Adding readonly here blocks editing: assigning value, calling clear() and every setSchema*() are ignored, and the change event never fires. Reading editor.value still works. Load with setInitialValue() instead.
Server-side rendering
Importing the package registers the custom element at module scope, so it needs a DOM and throws in Node. In Next.js, Nuxt, SvelteKit, or Astro, reach it from a client-only path.
useEffect(() => {
import('@dineug/erd-editor');
}, []);
TypeScript
Importing the package merges erd-editor into HTMLElementTagNameMap, so the element is typed without a cast.
import '@dineug/erd-editor';
import type { ErdEditorElement } from '@dineug/erd-editor';
const editor = document.createElement('erd-editor'); // ErdEditorElement
const found = document.querySelector('erd-editor'); // ErdEditorElement | null
ErdEditorElement is exported for annotating your own variables and props.
Syntax Highlighting
The Schema SQL and Code Generator panels are highlighted by Shiki, in a shared worker of its own.
Since 3.7.0 there is nothing to install and nothing to register: the worker is built the first time a code panel renders, so a page that opens none never fetches the grammars.
| Languages | SQL, TypeScript, GraphQL, C#, Java, Kotlin, Scala, Go, Python |
| Themes | github-dark and github-light, following the editor's light or dark appearance |
Those are exactly the languages the panels emit: JPA is highlighted as Java, SQLAlchemy as Python, TypeORM, Sequelize and Drizzle as TypeScript, and DBML and AML as SQL, the closest grammar in the bundle — see Code Generator.
The regex engine is plain JavaScript, so no host policy needs wasm-unsafe-eval.
Where SharedWorker is missing — Chrome on Android, Safari before 16.4 — the failure is logged and the panels render as plain text. Nothing else is affected.
Upgrading from 3.6.0 or earlier: @dineug/erd-editor-shiki-worker is no longer published, and setGetShikiServiceCallback is gone with it.
Drop the install and the registration; a page that loaded the worker from a CDN drops that second <script> tag as well.
Web Workers
The editor runs four jobs in SharedWorkers: syntax highlighting, PNG export, the table layout behind Auto Layout and the Visualization tab's Flow mode, and the document's own garbage collection.
All four ship inside @dineug/erd-editor. None of them is something you set up, but each one is something a host can block.
| Worker | Without it |
|---|---|
| Syntax highlighting | The Schema SQL and Code Generator panels stay plain text |
| PNG export | The image is drawn on the main thread, which blocks the page while it draws |
| Table layout | Auto Layout's Flow, Tree - vertical and Tree - horizontal layouts, and the Visualization tab's Flow mode, end in Could not place tables; Force and Graph mode are unaffected, since they run inside the editor |
| Schema garbage collection | It runs in-process |
PNG export and schema garbage collection wait ten seconds for a worker to answer and then carry on without it, and syntax highlighting leaves the panels as plain text as soon as its worker fails, so a host that blocks workers costs performance rather than function.
Table layout is the exception: the engine that computes a layout outweighs the editor itself and is never loaded in-process, so it waits thirty seconds for its worker on the first placement and reports a failure if none answers.
Once the worker has answered, a layout that has not come back within sixty seconds is given up on too, with the same Could not place tables. Auto Layout shows Placing tables… as soon as it starts, while Flow mode shows it only once a placement, its worker's start included, has run for six seconds.
In the bundled build all four are emitted as files beside the package, so a strict CSP needs worker-src 'self' — plus blob: where your bundler inlines a worker.
In the script-tag build all four travel inside the file as data: URLs, so that page needs worker-src data: instead.
Entry Points
Importing @dineug/erd-editor registers <erd-editor> as a side effect. Beyond that it exports the element type and two callback setters.
| Export | Description |
|---|---|
ErdEditorElement (type) | The element interface — see ErdEditorElement. |
setExportFileCallback(cb) | Replaces the browser download, (blob, { fileName }) => void. |
setImportFileCallback(cb) | Replaces the browser file picker, ({ type, op, accept }) => void. |
@dineug/erd-editor/engine.js is a second entry point. It runs the document store with no DOM, so it works in a Web Worker — see Remote Storage.
File Dialogs
Import and export go through injectable callbacks, so a host without a browser file dialog — an IDE webview, for example — can supply its own. The two are not symmetric: export hands you the finished file, while import only asks for one, and you push the content back in yourself.
op is set or diff — a diff goes to setDiffValue(), whatever the type. Otherwise type picks the method, and accept carries that type's extensions, ready to hand to a host file dialog.
type | accept | Method |
|---|---|---|
json | .json | editor.value = text |
sql | .sql | editor.setSchemaSQL(text) |
graphql | .graphql,.gql,.graphqls | editor.setSchemaGraphQL(text) |
dbml | .dbml | editor.setSchemaDBML(text) |
aml | .aml | editor.setSchemaAML(text) |
import { setExportFileCallback, setImportFileCallback } from '@dineug/erd-editor';
setExportFileCallback((blob, { fileName }) => host.writeFile(fileName, blob));
setImportFileCallback(async ({ type, op, accept }) => {
const text = await host.pickFile(accept);
if (op === 'diff') {
editor.setDiffValue(text);
} else if (type === 'json') {
editor.value = text;
} else if (type === 'sql') {
editor.setSchemaSQL(text);
} else if (type === 'graphql') {
editor.setSchemaGraphQL(text);
} else if (type === 'dbml') {
editor.setSchemaDBML(text);
} else if (type === 'aml') {
editor.setSchemaAML(text);
}
});
Dispatch on every type you handle and ignore the rest.
Assigning value clears the document before it parses, so routing a payload there that is not an .erd.json document — through a catch-all else, or because a type added later fell through — empties the diagram instead of importing anything.
The fileName the editor generates is <database name>-<timestamp> plus .erd.json, .sql, or .png, where the timestamp is formatted yyyy-MM-dd'T'HH_mm_ss. A blank database name falls back to unnamed.
Left unset, the editor uses the browser's own download and file-picker behavior. Pass null to go back to it.
Browser Support
Chrome 91+, Edge 94+, Firefox 93+, Safari 16.4+ — the ES2022 baseline the published bundles are built against. No polyfills are bundled. Add your own if you need to reach older browsers.